Merge branch 'develop' into subusers

This commit is contained in:
Charles Morgan 2020-10-31 17:32:10 -04:00 committed by GitHub
commit aad3019747
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 1375 additions and 753 deletions

View file

@ -3,6 +3,43 @@ 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. This project follows [Semantic Versioning](http://semver.org) guidelines.
## v1.0.3
### Fixed
* Fixes bug causing subusers to not be creatable or editable via the frontend for servers.
* Fixes system timezone not being passed along properly to the MySQL connection causing scheduled tasks to run every minute when the MySQL instance and Panel timezone did not line up.
* Fixes listing servers owned by a user in the admin area to actually list their servers.
### Changed
* Adds SameSite `lax` attribute for cookies generated by the Panel.
* Adds better filtering for searching servers in the admin area to better key off name, uuid, or owner username/email.
## v1.0.2
### Added
* Adds support for searching inside the file editor.
* Adds support for manually executing a schedule regardless of if it is currently queued or not.
* Adds an indicator to the schedule UI to show when a schedule is currently processing.
* Adds support for setting the `backup_limit` of a server via the API.
* **[Security]** Adds login throttling to the 2FA verification endpoint.
### Fixed
* Fixes subuser page title missing server name.
* Fixes schedule task `sequence_id` not properly being reset when a schedule's task is deleted.
* Fixes misc. UI bugs throughout the frontend when long text overflows its bounds.
* Fixes user deletion command to properly handle email & ID searching.
* Fixes color issues in the terminal causing certain text & background combinations to be illegible.
* Fixes reCAPTCHA not properly resetting on login failure.
* Fixes error messages not properly resetting between login screens.
* Fixes a UI crash when attempting to create or view a directory or file that contained the `%` somewhere in the name.
### Changed
* Updated the search modal to close itself when the ESC key is pressed.
* Updates the schedule view and editing UI to better display information to users.
* Changed logic powering server searching on the frontend to return more accurate results and return all servers when executing the query as an admin.
* Admin CP link no longer opens in a new tab.
* Mounts will no longer allow a user to mount certain directory combinations. This blocks mounting one server's files into another server, and blocks using the server data directory as a mount destination.
* Cleaned up assorted server build modification code.
* Updates default eggs to have improved install scripts and more consistent container usage.
## v1.0.1 ## v1.0.1
### Fixed ### Fixed
* Fixes 500 error when mounting a mount to a server, and other related errors when handling mounts. * Fixes 500 error when mounting a mount to a server, and other related errors when handling mounts.

View file

@ -15,7 +15,7 @@ Stop settling for less. Make game servers a first class citizen on your platform
![Image](https://cdn.pterodactyl.io/site-assets/pterodactyl_v1_demo.gif) ![Image](https://cdn.pterodactyl.io/site-assets/pterodactyl_v1_demo.gif)
## Sponsors ## Sponsors
I would like to extend my sincere thanks to the following sponsors for helping find Pterodactyl's developement. I would like to extend my sincere thanks to the following sponsors for helping fund Pterodactyl's developement.
[Interested in becoming a sponsor?](https://github.com/sponsors/DaneEveritt) [Interested in becoming a sponsor?](https://github.com/sponsors/DaneEveritt)
| Company | About | | Company | About |
@ -28,6 +28,8 @@ I would like to extend my sincere thanks to the following sponsors for helping f
| [**XCORE-SERVER.de**](https://xcore-server.de/) | XCORE-SERVER.de offers High-End Servers for hosting and gaming since 2012. Fast, excellent and well-known for eSports Gaming. | | [**XCORE-SERVER.de**](https://xcore-server.de/) | XCORE-SERVER.de offers High-End Servers for hosting and gaming since 2012. Fast, excellent and well-known for eSports Gaming. |
| [**RoyaleHosting**](https://royalehosting.net/) | Build your dreams and deploy them with RoyaleHostings reliable servers and network. Easy to use, provisioned in a couple of minutes. | | [**RoyaleHosting**](https://royalehosting.net/) | Build your dreams and deploy them with RoyaleHostings reliable servers and network. Easy to use, provisioned in a couple of minutes. |
| [**Spill Hosting**](https://spillhosting.no/) | Spill Hosting is a Norwegian hosting service, which aims to cheap services on quality servers. Premium i9-9900K processors will run your game like a dream. | | [**Spill Hosting**](https://spillhosting.no/) | Spill Hosting is a Norwegian hosting service, which aims to cheap 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. |
## Documentation ## Documentation
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html) * [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)

View file

@ -144,6 +144,11 @@ class AppSettingsCommand extends Command
$this->variables['APP_ENVIRONMENT_ONLY'] = $this->confirm(trans('command/messages.environment.app.settings'), true) ? 'false' : 'true'; $this->variables['APP_ENVIRONMENT_ONLY'] = $this->confirm(trans('command/messages.environment.app.settings'), true) ? 'false' : 'true';
} }
// Make sure session cookies are set as "secure" when using HTTPS
if (strpos($this->variables['APP_URL'], 'https://') === 0) {
$this->variables['SESSION_SECURE_COOKIE'] = 'true';
}
$this->checkForRedis(); $this->checkForRedis();
$this->writeToEnvironment($this->variables); $this->writeToEnvironment($this->variables);

View file

@ -1,62 +1,37 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Console\Commands\Schedule; namespace Pterodactyl\Console\Commands\Schedule;
use Cake\Chronos\Chronos; use Throwable;
use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Collection; use Pterodactyl\Models\Schedule;
use Illuminate\Support\Facades\Log;
use Pterodactyl\Services\Schedules\ProcessScheduleService; use Pterodactyl\Services\Schedules\ProcessScheduleService;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
class ProcessRunnableCommand extends Command class ProcessRunnableCommand extends Command
{ {
/** /**
* @var string * @var string
*/ */
protected $description = 'Process schedules in the database and determine which are ready to run.'; protected $signature = 'p:schedule:process';
/**
* @var \Pterodactyl\Services\Schedules\ProcessScheduleService
*/
protected $processScheduleService;
/**
* @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface
*/
protected $repository;
/** /**
* @var string * @var string
*/ */
protected $signature = 'p:schedule:process'; protected $description = 'Process schedules in the database and determine which are ready to run.';
/**
* ProcessRunnableCommand constructor.
*
* @param \Pterodactyl\Services\Schedules\ProcessScheduleService $processScheduleService
* @param \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface $repository
*/
public function __construct(ProcessScheduleService $processScheduleService, ScheduleRepositoryInterface $repository)
{
parent::__construct();
$this->processScheduleService = $processScheduleService;
$this->repository = $repository;
}
/** /**
* Handle command execution. * Handle command execution.
*/ */
public function handle() public function handle()
{ {
$schedules = $this->repository->getSchedulesToProcess(Chronos::now()->toAtomString()); $schedules = Schedule::query()->with('tasks')
->where('is_active', true)
->where('is_processing', false)
->whereRaw('next_run_at <= NOW()')
->get();
if ($schedules->count() < 1) { if ($schedules->count() < 1) {
$this->line('There are no scheduled tasks for servers that need to be run.'); $this->line('There are no scheduled tasks for servers that need to be run.');
@ -64,23 +39,41 @@ class ProcessRunnableCommand extends Command
} }
$bar = $this->output->createProgressBar(count($schedules)); $bar = $this->output->createProgressBar(count($schedules));
$schedules->each(function ($schedule) use ($bar) { foreach ($schedules as $schedule) {
if ($schedule->tasks instanceof Collection && count($schedule->tasks) > 0) {
$this->processScheduleService->handle($schedule);
if ($this->input->isInteractive()) {
$bar->clear(); $bar->clear();
$this->processSchedule($schedule);
$bar->advance();
$bar->display();
}
$this->line('');
}
/**
* Processes a given schedule and logs and errors encountered the console output. This should
* never throw an exception out, otherwise you'll end up killing the entire run group causing
* any other schedules to not process correctly.
*
* @param \Pterodactyl\Models\Schedule $schedule
* @see https://github.com/pterodactyl/panel/issues/2609
*/
protected function processSchedule(Schedule $schedule)
{
if ($schedule->tasks->isEmpty()) {
return;
}
try {
$this->getLaravel()->make(ProcessScheduleService::class)->handle($schedule);
$this->line(trans('command/messages.schedule.output_line', [ $this->line(trans('command/messages.schedule.output_line', [
'schedule' => $schedule->name, 'schedule' => $schedule->name,
'hash' => $schedule->hashid, 'hash' => $schedule->hashid,
])); ]));
} } catch (Throwable | Exception $exception) {
} Log::error($exception, ['schedule_id' => $schedule->id]);
$bar->advance(); $this->error("An error was encountered while processing Schedule #{$schedule->id}: " . $exception->getMessage());
$bar->display(); }
});
$this->line('');
} }
} }

View file

@ -24,12 +24,4 @@ interface ScheduleRepositoryInterface extends RepositoryInterface
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function getScheduleWithTasks(int $schedule): Schedule; public function getScheduleWithTasks(int $schedule): Schedule;
/**
* Return all of the schedules that should be processed.
*
* @param string $timestamp
* @return \Illuminate\Support\Collection
*/
public function getSchedulesToProcess(string $timestamp): Collection;
} }

View file

@ -139,16 +139,4 @@ interface ServerRepositoryInterface extends RepositoryInterface
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/ */
public function loadAllServersForNode(int $node, int $limit): LengthAwarePaginator; public function loadAllServersForNode(int $node, int $limit): LengthAwarePaginator;
/**
* Returns every server that exists for a given node.
*
* This is different from {@see loadAllServersForNode} because
* it does not paginate the response.
*
* @param int $node
*
* @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection
*/
public function loadEveryServerForNode(int $node);
} }

25
app/Helpers/Time.php Normal file
View file

@ -0,0 +1,25 @@
<?php
namespace Pterodactyl\Helpers;
use Carbon\CarbonImmutable;
final class Time
{
/**
* Gets the time offset from the provided timezone relative to UTC as a number. This
* is used in the database configuration since we can't always rely on there being support
* for named timezones in MySQL.
*
* Returns the timezone as a string like +08:00 or -05:00 depending on the app timezone.
*
* @param string $timezone
* @return string
*/
public static function getMySQLTimezoneOffset(string $timezone): string
{
$offset = round(CarbonImmutable::now($timezone)->getTimezone()->getOffset(CarbonImmutable::now('UTC')) / 3600);
return sprintf('%s%s:00', $offset > 0 ? '+' : '-', str_pad(abs($offset), 2, '0', STR_PAD_LEFT));
}
}

View file

@ -52,7 +52,12 @@ class Utilities
)->getNextRunDate()); )->getNextRunDate());
} }
public static function checked($name, $default) /**
* @param string $name
* @param mixed $default
* @return string
*/
public static function checked(string $name, $default)
{ {
$errors = session('errors'); $errors = session('errors');

View file

@ -6,7 +6,9 @@ use Illuminate\Http\Request;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Spatie\QueryBuilder\QueryBuilder; use Spatie\QueryBuilder\QueryBuilder;
use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\Factory;
use Spatie\QueryBuilder\AllowedFilter;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Models\Filters\AdminServerFilter;
use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Repositories\Eloquent\ServerRepository;
class ServerController extends Controller class ServerController extends Controller
@ -45,8 +47,10 @@ class ServerController extends Controller
public function index(Request $request) public function index(Request $request)
{ {
$servers = QueryBuilder::for(Server::query()->with('node', 'user', 'allocation')) $servers = QueryBuilder::for(Server::query()->with('node', 'user', 'allocation'))
->allowedFilters(['uuid', 'name', 'image']) ->allowedFilters([
->allowedSorts(['id', 'uuid']) AllowedFilter::exact('owner_id'),
AllowedFilter::custom('*', new AdminServerFilter),
])
->paginate(config()->get('pterodactyl.paginate.admin.servers')); ->paginate(config()->get('pterodactyl.paginate.admin.servers'));
return $this->view->make('admin.servers.index', ['servers' => $servers]); return $this->view->make('admin.servers.index', ['servers' => $servers]);

View file

@ -56,7 +56,8 @@ class ScheduleTaskController extends ClientApiController
); );
} }
$lastTask = $schedule->tasks->last(); /** @var \Pterodactyl\Models\Task|null $lastTask */
$lastTask = $schedule->tasks()->orderByDesc('sequence_id')->first();
/** @var \Pterodactyl\Models\Task $task */ /** @var \Pterodactyl\Models\Task $task */
$task = $this->repository->create([ $task = $this->repository->create([
@ -102,13 +103,16 @@ class ScheduleTaskController extends ClientApiController
} }
/** /**
* Determines if a user can delete the task for a given server. * Delete a given task for a schedule. If there are subsequent tasks stored in the database
* for this schedule their sequence IDs are decremented properly.
* *
* @param \Pterodactyl\Http\Requests\Api\Client\ClientApiRequest $request * @param \Pterodactyl\Http\Requests\Api\Client\ClientApiRequest $request
* @param \Pterodactyl\Models\Server $server * @param \Pterodactyl\Models\Server $server
* @param \Pterodactyl\Models\Schedule $schedule * @param \Pterodactyl\Models\Schedule $schedule
* @param \Pterodactyl\Models\Task $task * @param \Pterodactyl\Models\Task $task
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
*
* @throws \Exception
*/ */
public function delete(ClientApiRequest $request, Server $server, Schedule $schedule, Task $task) public function delete(ClientApiRequest $request, Server $server, Schedule $schedule, Task $task)
{ {
@ -120,8 +124,12 @@ class ScheduleTaskController extends ClientApiController
throw new HttpForbiddenException('You do not have permission to perform this action.'); throw new HttpForbiddenException('You do not have permission to perform this action.');
} }
$this->repository->delete($task->id); $schedule->tasks()->where('sequence_id', '>', $task->sequence_id)->update([
'sequence_id' => $schedule->tasks()->getConnection()->raw('(sequence_id - 1)'),
]);
return JsonResponse::create(null, Response::HTTP_NO_CONTENT); $task->delete();
return new JsonResponse(null, Response::HTTP_NO_CONTENT);
} }
} }

View file

@ -3,11 +3,14 @@
namespace Pterodactyl\Http\Controllers\Api\Remote\Servers; namespace Pterodactyl\Http\Controllers\Api\Remote\Servers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Pterodactyl\Models\Server;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Repositories\Eloquent\NodeRepository; use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Services\Eggs\EggConfigurationService; use Pterodactyl\Services\Eggs\EggConfigurationService;
use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Http\Resources\Wings\ServerConfigurationCollection;
use Pterodactyl\Services\Servers\ServerConfigurationStructureService; use Pterodactyl\Services\Servers\ServerConfigurationStructureService;
class ServerDetailsController extends Controller class ServerDetailsController extends Controller
@ -27,11 +30,6 @@ class ServerDetailsController extends Controller
*/ */
private $configurationStructureService; private $configurationStructureService;
/**
* @var \Pterodactyl\Repositories\Eloquent\NodeRepository
*/
private $nodeRepository;
/** /**
* ServerConfigurationController constructor. * ServerConfigurationController constructor.
* *
@ -49,7 +47,6 @@ class ServerDetailsController extends Controller
$this->eggConfigurationService = $eggConfigurationService; $this->eggConfigurationService = $eggConfigurationService;
$this->repository = $repository; $this->repository = $repository;
$this->configurationStructureService = $configurationStructureService; $this->configurationStructureService = $configurationStructureService;
$this->nodeRepository = $nodeRepository;
} }
/** /**
@ -66,7 +63,7 @@ class ServerDetailsController extends Controller
{ {
$server = $this->repository->getByUuid($uuid); $server = $this->repository->getByUuid($uuid);
return JsonResponse::create([ return new JsonResponse([
'settings' => $this->configurationStructureService->handle($server), 'settings' => $this->configurationStructureService->handle($server),
'process_configuration' => $this->eggConfigurationService->handle($server), 'process_configuration' => $this->eggConfigurationService->handle($server),
]); ]);
@ -76,25 +73,19 @@ class ServerDetailsController extends Controller
* Lists all servers with their configurations that are assigned to the requesting node. * Lists all servers with their configurations that are assigned to the requesting node.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* * @return \Pterodactyl\Http\Resources\Wings\ServerConfigurationCollection
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function list(Request $request) public function list(Request $request)
{ {
/** @var \Pterodactyl\Models\Node $node */
$node = $request->attributes->get('node'); $node = $request->attributes->get('node');
$servers = $this->repository->loadEveryServerForNode($node->id);
$configurations = []; // Avoid run-away N+1 SQL queries by pre-loading the relationships that are used
// within each of the services called below.
$servers = Server::query()->with('allocations', 'egg', 'mounts', 'variables', 'location')
->where('node_id', $node->id)
->paginate($request->input('per_page', 50));
foreach ($servers as $server) { return new ServerConfigurationCollection($servers);
$configurations[$server->uuid] = [
'settings' => $this->configurationStructureService->handle($server),
'process_configuration' => $this->eggConfigurationService->handle($server),
];
}
return JsonResponse::create($configurations);
} }
} }

View file

@ -69,7 +69,7 @@ class DaemonAuthenticate
// Ensure that all of the correct parts are provided in the header. // Ensure that all of the correct parts are provided in the header.
if (count($parts) !== 2 || empty($parts[0]) || empty($parts[1])) { if (count($parts) !== 2 || empty($parts[0]) || empty($parts[1])) {
throw new BadRequestHttpException( throw new BadRequestHttpException(
'The Authorization headed provided was not in a valid format.' 'The Authorization header provided was not in a valid format.'
); );
} }

View file

@ -0,0 +1,35 @@
<?php
namespace Pterodactyl\Http\Resources\Wings;
use Pterodactyl\Models\Server;
use Illuminate\Container\Container;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Pterodactyl\Services\Eggs\EggConfigurationService;
use Pterodactyl\Services\Servers\ServerConfigurationStructureService;
class ServerConfigurationCollection extends ResourceCollection
{
/**
* Converts a collection of Server models into an array of configuration responses
* that can be understood by Wings. Make sure you've properly loaded the required
* relationships on the Server models before calling this function, otherwise you'll
* have some serious performance issues from all of the N+1 queries.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
$egg = Container::getInstance()->make(EggConfigurationService::class);
$configuration = Container::getInstance()->make(ServerConfigurationStructureService::class);
return $this->collection->map(function (Server $server) use ($configuration, $egg) {
return [
'uuid' => $server->uuid,
'settings' => $configuration->handle($server),
'process_configuration' => $egg->handle($server),
];
})->toArray();
}
}

View file

@ -7,11 +7,12 @@ use Pterodactyl\Jobs\Job;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
use Pterodactyl\Models\Task; use Pterodactyl\Models\Task;
use InvalidArgumentException; use InvalidArgumentException;
use Pterodactyl\Models\Schedule;
use Illuminate\Support\Facades\Log;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Pterodactyl\Repositories\Eloquent\TaskRepository;
use Pterodactyl\Services\Backups\InitiateBackupService; use Pterodactyl\Services\Backups\InitiateBackupService;
use Pterodactyl\Repositories\Wings\DaemonPowerRepository; use Pterodactyl\Repositories\Wings\DaemonPowerRepository;
use Pterodactyl\Repositories\Wings\DaemonCommandRepository; use Pterodactyl\Repositories\Wings\DaemonCommandRepository;

View file

@ -0,0 +1,40 @@
<?php
namespace Pterodactyl\Models\Filters;
use BadMethodCallException;
use Spatie\QueryBuilder\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
class AdminServerFilter implements Filter
{
/**
* A multi-column filter for the servers table that allows an administrative user to search
* across UUID, name, owner username, and owner email.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $value
* @param string $property
*/
public function __invoke(Builder $query, $value, string $property)
{
if ($query->getQuery()->from !== 'servers') {
throw new BadMethodCallException(
'Cannot use the AdminServerFilter against a non-server model.'
);
}
$query
->select('servers.*')
->leftJoin('users', 'users.id', '=', 'servers.owner_id')
->where(function (Builder $builder) use ($value) {
$builder->where('servers.uuid', $value)
->orWhere('servers.uuid', 'LIKE', "$value%")
->orWhere('servers.uuidShort', $value)
->orWhere('servers.external_id', $value)
->orWhereRaw('LOWER(users.username) LIKE ?', ["%$value%"])
->orWhereRaw('LOWER(users.email) LIKE ?', ["$value%"])
->orWhereRaw('LOWER(servers.name) LIKE ?', ["%$value%"]);
})
->groupBy('servers.id');
}
}

View file

@ -83,6 +83,13 @@ class Server extends Model
'oom_disabled' => true, 'oom_disabled' => true,
]; ];
/**
* The default relationships to load for all server models.
*
* @var string[]
*/
protected $with = ['allocation'];
/** /**
* The attributes that should be mutated to dates. * The attributes that should be mutated to dates.
* *

View file

@ -47,19 +47,4 @@ class ScheduleRepository extends EloquentRepository implements ScheduleRepositor
throw new RecordNotFoundException; throw new RecordNotFoundException;
} }
} }
/**
* Return all of the schedules that should be processed.
*
* @param string $timestamp
* @return \Illuminate\Support\Collection
*/
public function getSchedulesToProcess(string $timestamp): Collection
{
return $this->getBuilder()->with('tasks')
->where('is_active', true)
->where('is_processing', false)
->where('next_run_at', '<=', $timestamp)
->get($this->getColumns());
}
} }

View file

@ -270,22 +270,4 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
->where('node_id', '=', $node) ->where('node_id', '=', $node)
->paginate($limit); ->paginate($limit);
} }
/**
* Returns every server that exists for a given node.
*
* This is different from {@see loadAllServersForNode} because
* it does not paginate the response.
*
* @param int $node
*
* @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection
*/
public function loadEveryServerForNode(int $node)
{
return $this->getBuilder()
->with('nest')
->where('node_id', '=', $node)
->get();
}
} }

View file

@ -2,6 +2,7 @@
namespace Pterodactyl\Services\Schedules; namespace Pterodactyl\Services\Schedules;
use Exception;
use Pterodactyl\Models\Schedule; use Pterodactyl\Models\Schedule;
use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Bus\Dispatcher;
use Pterodactyl\Jobs\Schedule\RunTaskJob; use Pterodactyl\Jobs\Schedule\RunTaskJob;
@ -43,7 +44,7 @@ class ProcessScheduleService
public function handle(Schedule $schedule, bool $now = false) public function handle(Schedule $schedule, bool $now = false)
{ {
/** @var \Pterodactyl\Models\Task $task */ /** @var \Pterodactyl\Models\Task $task */
$task = $schedule->tasks()->where('sequence_id', 1)->first(); $task = $schedule->tasks()->orderBy('sequence_id', 'asc')->first();
if (is_null($task)) { if (is_null($task)) {
throw new DisplayException( throw new DisplayException(
@ -60,8 +61,22 @@ class ProcessScheduleService
$task->update(['is_queued' => true]); $task->update(['is_queued' => true]);
}); });
$this->dispatcher->{$now ? 'dispatchNow' : 'dispatch'}( $job = new RunTaskJob($task);
(new RunTaskJob($task))->delay($task->time_offset)
); if (! $now) {
$this->dispatcher->dispatch($job->delay($task->time_offset));
} else {
// When using dispatchNow the RunTaskJob::failed() function is not called automatically
// so we need to manually trigger it and then continue with the exception throw.
//
// @see https://github.com/pterodactyl/panel/issues/2550
try {
$this->dispatcher->dispatchNow($job);
} catch (Exception $exception) {
$job->failed($exception);
throw $exception;
}
}
} }
} }

View file

@ -4,22 +4,14 @@ namespace Pterodactyl\Services\Servers;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use GuzzleHttp\Exception\RequestException; use Pterodactyl\Models\Allocation;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\DisplayException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Pterodactyl\Repositories\Wings\DaemonServerRepository; use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
class BuildModificationService class BuildModificationService
{ {
/**
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
*/
private $allocationRepository;
/** /**
* @var \Illuminate\Database\ConnectionInterface * @var \Illuminate\Database\ConnectionInterface
*/ */
@ -30,11 +22,6 @@ class BuildModificationService
*/ */
private $daemonServerRepository; private $daemonServerRepository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $repository;
/** /**
* @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService * @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService
*/ */
@ -43,23 +30,17 @@ class BuildModificationService
/** /**
* BuildModificationService constructor. * BuildModificationService constructor.
* *
* @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository
* @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $structureService * @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $structureService
* @param \Illuminate\Database\ConnectionInterface $connection * @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository * @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
*/ */
public function __construct( public function __construct(
AllocationRepositoryInterface $allocationRepository,
ServerConfigurationStructureService $structureService, ServerConfigurationStructureService $structureService,
ConnectionInterface $connection, ConnectionInterface $connection,
DaemonServerRepository $daemonServerRepository, DaemonServerRepository $daemonServerRepository
ServerRepositoryInterface $repository
) { ) {
$this->allocationRepository = $allocationRepository;
$this->daemonServerRepository = $daemonServerRepository; $this->daemonServerRepository = $daemonServerRepository;
$this->connection = $connection; $this->connection = $connection;
$this->repository = $repository;
$this->structureService = $structureService; $this->structureService = $structureService;
} }
@ -70,9 +51,8 @@ class BuildModificationService
* @param array $data * @param array $data
* @return \Pterodactyl\Models\Server * @return \Pterodactyl\Models\Server
* *
* @throws \Throwable
* @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/ */
public function handle(Server $server, array $data) public function handle(Server $server, array $data)
{ {
@ -82,48 +62,35 @@ class BuildModificationService
if (isset($data['allocation_id']) && $data['allocation_id'] != $server->allocation_id) { if (isset($data['allocation_id']) && $data['allocation_id'] != $server->allocation_id) {
try { try {
$this->allocationRepository->findFirstWhere([ Allocation::query()->where('id', $data['allocation_id'])->where('server_id', $server->id)->firstOrFail();
['id', '=', $data['allocation_id']], } catch (ModelNotFoundException $ex) {
['server_id', '=', $server->id], throw new DisplayException('The requested default allocation is not currently assigned to this server.');
]);
} catch (RecordNotFoundException $ex) {
throw new DisplayException(trans('admin/server.exceptions.default_allocation_not_found'));
} }
} }
/* @var \Pterodactyl\Models\Server $server */ // If any of these values are passed through in the data array go ahead and set
$server = $this->repository->withFreshModel()->update($server->id, [ // them correctly on the server model.
'oom_disabled' => array_get($data, 'oom_disabled'), $merge = Arr::only($data, ['oom_disabled', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'allocation_id']);
'memory' => array_get($data, 'memory'),
'swap' => array_get($data, 'swap'), $server->forceFill(array_merge($merge, [
'io' => array_get($data, 'io'), 'database_limit' => Arr::get($data, 'database_limit', 0) ?? null,
'cpu' => array_get($data, 'cpu'), 'allocation_limit' => Arr::get($data, 'allocation_limit', 0) ?? null,
'threads' => array_get($data, 'threads'), 'backup_limit' => Arr::get($data, 'backup_limit', 0) ?? 0,
'disk' => array_get($data, 'disk'), ]))->saveOrFail();
'allocation_id' => array_get($data, 'allocation_id'),
'database_limit' => array_get($data, 'database_limit', 0) ?? null, $server = $server->fresh();
'allocation_limit' => array_get($data, 'allocation_limit', 0) ?? null,
'backup_limit' => array_get($data, 'backup_limit', 0) ?? null,
]);
$updateData = $this->structureService->handle($server); $updateData = $this->structureService->handle($server);
try { $this->daemonServerRepository->setServer($server)->update($updateData['build'] ?? []);
$this->daemonServerRepository
->setServer($server)
->update(Arr::only($updateData, ['build']));
$this->connection->commit(); $this->connection->commit();
} catch (RequestException $exception) {
throw new DaemonConnectionException($exception);
}
return $server; return $server;
} }
/** /**
* Process the allocations being assigned in the data and ensure they * Process the allocations being assigned in the data and ensure they are available for a server.
* are available for a server.
* *
* @param \Pterodactyl\Models\Server $server * @param \Pterodactyl\Models\Server $server
* @param array $data * @param array $data
@ -132,55 +99,53 @@ class BuildModificationService
*/ */
private function processAllocations(Server $server, array &$data) private function processAllocations(Server $server, array &$data)
{ {
$firstAllocationId = null; if (empty($data['add_allocations']) && empty($data['remove_allocations'])) {
if (! array_key_exists('add_allocations', $data) && ! array_key_exists('remove_allocations', $data)) {
return; return;
} }
// Handle the addition of allocations to this server. // Handle the addition of allocations to this server. Only assign allocations that are not currently
if (array_key_exists('add_allocations', $data) && ! empty($data['add_allocations'])) { // assigned to a different server, and only allocations on the same node as the server.
$unassigned = $this->allocationRepository->getUnassignedAllocationIds($server->node_id); if (! empty($data['add_allocations'])) {
$query = Allocation::query()
->where('node_id', $server->node_id)
->whereIn('id', $data['add_allocations'])
->whereNull('server_id');
$updateIds = []; // Keep track of all the allocations we're just now adding so that we can use the first
foreach ($data['add_allocations'] as $allocation) { // one to reset the default allocation to.
if (! in_array($allocation, $unassigned)) { $freshlyAllocated = $query->pluck('id')->first();
continue;
$query->update(['server_id' => $server->id, 'notes' => null]);
} }
$firstAllocationId = $firstAllocationId ?? $allocation; if (! empty($data['remove_allocations'])) {
$updateIds[] = $allocation;
}
if (! empty($updateIds)) {
$this->allocationRepository->updateWhereIn('id', $updateIds, ['server_id' => $server->id]);
}
}
// Handle removal of allocations from this server.
if (array_key_exists('remove_allocations', $data) && ! empty($data['remove_allocations'])) {
$assigned = $server->allocations->pluck('id')->toArray();
$updateIds = [];
foreach ($data['remove_allocations'] as $allocation) { foreach ($data['remove_allocations'] as $allocation) {
if (! in_array($allocation, $assigned)) { // If we are attempting to remove the default allocation for the server, see if we can reassign
continue; // to the first provided value in add_allocations. If there is no new first allocation then we
// will throw an exception back.
if ($allocation === ($data['allocation_id'] ?? $server->allocation_id)) {
if (empty($freshlyAllocated)) {
throw new DisplayException(
'You are attempting to delete the default allocation for this server but there is no fallback allocation to use.'
);
} }
if ($allocation == $data['allocation_id']) { // Update the default allocation to be the first allocation that we are creating.
if (is_null($firstAllocationId)) { $data['allocation_id'] = $freshlyAllocated;
throw new DisplayException(trans('admin/server.exceptions.no_new_default_allocation')); }
} }
$data['allocation_id'] = $firstAllocationId; // Remove any of the allocations we got that are currently assigned to this server on
} // this node. Also set the notes to null, otherwise when re-allocated to a new server those
// notes will be carried over.
$updateIds[] = $allocation; Allocation::query()->where('node_id', $server->node_id)
} ->where('server_id', $server->id)
// Only remove the allocations that we didn't also attempt to add to the server...
if (! empty($updateIds)) { ->whereIn('id', array_diff($data['remove_allocations'], $data['add_allocations'] ?? []))
$this->allocationRepository->updateWhereIn('id', $updateIds, ['server_id' => null]); ->update([
} 'notes' => null,
'server_id' => null,
]);
} }
} }
} }

View file

@ -7,8 +7,6 @@ use Pterodactyl\Models\Server;
class ServerConfigurationStructureService class ServerConfigurationStructureService
{ {
const REQUIRED_RELATIONS = ['allocation', 'allocations', 'egg'];
/** /**
* @var \Pterodactyl\Services\Servers\EnvironmentService * @var \Pterodactyl\Services\Servers\EnvironmentService
*/ */
@ -31,13 +29,11 @@ class ServerConfigurationStructureService
* daemon, if you modify the structure eggs will break unexpectedly. * daemon, if you modify the structure eggs will break unexpectedly.
* *
* @param \Pterodactyl\Models\Server $server * @param \Pterodactyl\Models\Server $server
* @param bool $legacy * @param bool $legacy deprecated
* @return array * @return array
*/ */
public function handle(Server $server, bool $legacy = false): array public function handle(Server $server, bool $legacy = false): array
{ {
$server->loadMissing(self::REQUIRED_RELATIONS);
return $legacy ? return $legacy ?
$this->returnLegacyFormat($server) $this->returnLegacyFormat($server)
: $this->returnCurrentFormat($server); : $this->returnCurrentFormat($server);
@ -93,6 +89,7 @@ class ServerConfigurationStructureService
* *
* @param \Pterodactyl\Models\Server $server * @param \Pterodactyl\Models\Server $server
* @return array * @return array
* @deprecated
*/ */
protected function returnLegacyFormat(Server $server) protected function returnLegacyFormat(Server $server)
{ {

View file

@ -33,6 +33,8 @@ return [
// backup for that server lives within that folder. // backup for that server lives within that folder.
'prefix' => env('AWS_BACKUPS_BUCKET') ?? '', 'prefix' => env('AWS_BACKUPS_BUCKET') ?? '',
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'use_accelerate_endpoint' => env('AWS_BACKUPS_USE_ACCELERATE', false), 'use_accelerate_endpoint' => env('AWS_BACKUPS_USE_ACCELERATE', false),
], ],
], ],

View file

@ -1,5 +1,7 @@
<?php <?php
use Pterodactyl\Helpers\Time;
return [ return [
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -43,6 +45,7 @@ return [
'collation' => 'utf8mb4_unicode_ci', 'collation' => 'utf8mb4_unicode_ci',
'prefix' => env('DB_PREFIX', ''), 'prefix' => env('DB_PREFIX', ''),
'strict' => env('DB_STRICT_MODE', false), 'strict' => env('DB_STRICT_MODE', false),
'timezone' => env('DB_TIMEZONE', Time::getMySQLTimezoneOffset(env('APP_TIMEZONE')))
], ],
/* /*
@ -65,6 +68,7 @@ return [
'collation' => 'utf8mb4_unicode_ci', 'collation' => 'utf8mb4_unicode_ci',
'prefix' => '', 'prefix' => '',
'strict' => false, 'strict' => false,
'timezone' => env('DB_TIMEZONE', Time::getMySQLTimezoneOffset(env('APP_TIMEZONE')))
], ],
], ],

View file

@ -58,6 +58,8 @@ return [
'secret' => env('AWS_SECRET_ACCESS_KEY'), 'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'), 'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'), 'bucket' => env('AWS_BUCKET'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
], ],
], ],
]; ];

View file

@ -188,5 +188,5 @@ return [
| |
*/ */
'same_site' => null, 'same_site' => env('SESSION_SAMESITE_COOKIE', 'lax'),
]; ];

View file

@ -0,0 +1,35 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class RemoveNullableFromScheduleNameField extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::update("UPDATE schedules SET name = 'Schedule' WHERE name IS NULL OR name = ''");
Schema::table('schedules', function (Blueprint $table) {
$table->string('name')->nullable(false)->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('schedules', function (Blueprint $table) {
$table->string('name')->nullable()->change();
});
}
}

View file

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2020-04-12T16:00:51-07:00", "exported_at": "2020-10-19T23:22:26+00:00",
"name": "Bungeecord", "name": "Bungeecord",
"author": "support@pterodactyl.io", "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.", "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.",
@ -17,9 +17,9 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/ash\n# Bungeecord Installation Script\n#\n# Server Files: \/mnt\/server\napk update\napk add curl\n\ncd \/mnt\/server\n\nif [ -z \"${BUNGEE_VERSION}\" ] || [ \"${BUNGEE_VERSION}\" == \"latest\" ]; then\n BUNGEE_VERSION=\"lastStableBuild\"\nfi\n\ncurl -o ${SERVER_JARFILE} https:\/\/ci.md-5.net\/job\/BungeeCord\/${BUNGEE_VERSION}\/artifact\/bootstrap\/target\/BungeeCord.jar", "script": "#!\/bin\/bash\r\n# Bungeecord Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napt update\r\napt install -y curl\r\n\r\ncd \/mnt\/server\r\n\r\nif [ -z \"${BUNGEE_VERSION}\" ] || [ \"${BUNGEE_VERSION}\" == \"latest\" ]; then\r\n BUNGEE_VERSION=\"lastStableBuild\"\r\nfi\r\n\r\ncurl -o ${SERVER_JARFILE} https:\/\/ci.md-5.net\/job\/BungeeCord\/${BUNGEE_VERSION}\/artifact\/bootstrap\/target\/BungeeCord.jar",
"container": "alpine:3.9", "container": "debian:buster-slim",
"entrypoint": "ash" "entrypoint": "bash"
} }
}, },
"variables": [ "variables": [
@ -28,8 +28,8 @@
"description": "The version of Bungeecord to download and use.", "description": "The version of Bungeecord to download and use.",
"env_variable": "BUNGEE_VERSION", "env_variable": "BUNGEE_VERSION",
"default_value": "latest", "default_value": "latest",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|alpha_num|between:1,6" "rules": "required|alpha_num|between:1,6"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The name of the Jarfile to use when running Bungeecord.", "description": "The name of the Jarfile to use when running Bungeecord.",
"env_variable": "SERVER_JARFILE", "env_variable": "SERVER_JARFILE",
"default_value": "bungeecord.jar", "default_value": "bungeecord.jar",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/" "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
} }
] ]

View file

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2020-05-24T12:15:13-04:00", "exported_at": "2020-10-19T23:22:28+00:00",
"name": "Forge Minecraft", "name": "Forge Minecraft",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "Minecraft Forge Server. Minecraft Forge is a modding API (Application Programming Interface), which makes it easier to create mods, and also make sure mods are compatible with each other.", "description": "Minecraft Forge Server. Minecraft Forge is a modding API (Application Programming Interface), which makes it easier to create mods, and also make sure mods are compatible with each other.",
@ -17,7 +17,7 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/bash\r\n# Forge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napt update\r\napt install -y curl jq\r\n\r\n#Go into main direction\r\nif [ ! -d \/mnt\/server ]; then\r\n mkdir \/mnt\/server\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\nif [ ! -z ${FORGE_VERSION} ]; then\r\n DOWNLOAD_LINK=https:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/${FORGE_VERSION}\/forge-${FORGE_VERSION}\r\nelse\r\n JSON_DATA=$(curl -sSL https:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/promotions_slim.json)\r\n\r\n if [ \"${MC_VERSION}\" == \"latest\" ] || [ \"${MC_VERSION}\" == \"\" ] ; then\r\n echo -e \"getting latest recommended version of forge.\"\r\n MC_VERSION=$(echo -e ${JSON_DATA} | jq -r '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains(\"recommended\")) | split(\"-\")[0]' | sort -t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n | tail -1)\r\n \tBUILD_TYPE=recommended\r\n fi\r\n\r\n if [ \"${BUILD_TYPE}\" != \"recommended\" ] && [ \"${BUILD_TYPE}\" != \"latest\" ]; then\r\n BUILD_TYPE=recommended\r\n fi\r\n\r\n echo -e \"minecraft version: ${MC_VERSION}\"\r\n echo -e \"build type: ${BUILD_TYPE}\"\r\n\r\n ## some variables for getting versions and things\r\n FILE_SITE=$(echo -e ${JSON_DATA} | jq -r '.homepage' | sed \"s\/http:\/https:\/g\")\r\n VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION \"${MC_VERSION}\" --arg BUILD_TYPE \"${BUILD_TYPE}\" '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains($BUILD_TYPE))')\r\n\r\n ## locating the forge version\r\n if [ \"${VERSION_KEY}\" == \"\" ] && [ \"${BUILD_TYPE}\" == \"recommended\" ]; then\r\n echo -e \"dropping back to latest from recommended due to there not being a recommended version of forge for the mc version requested.\"\r\n VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION \"${MC_VERSION}\" '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains(\"recommended\"))')\r\n fi\r\n\r\n ## Error if the mc version set wasn't valid.\r\n if [ \"${VERSION_KEY}\" == \"\" ] || [ \"${VERSION_KEY}\" == \"null\" ]; then\r\n \techo -e \"The install failed because there is no valid version of forge for the version on minecraft selected.\"\r\n \texit 1\r\n fi\r\n\r\n FORGE_VERSION=$(echo -e ${JSON_DATA} | jq -r --arg VERSION_KEY \"$VERSION_KEY\" '.promos | .[$VERSION_KEY]')\r\n\r\n if [ \"${MC_VERSION}\" == \"1.7.10\" ] || [ \"${MC_VERSION}\" == \"1.8.9\" ]; then\r\n DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}\/forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}.jar\r\n if [ \"${MC_VERSION}\" == \"1.7.10\" ]; then\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}-universal.jar\r\n fi\r\n else\r\n DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}\/forge-${MC_VERSION}-${FORGE_VERSION}\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}.jar\r\n fi\r\nfi\r\n\r\n\r\n#Adding .jar when not eding by SERVER_JARFILE\r\nif [[ ! $SERVER_JARFILE = *\\.jar ]]; then\r\n SERVER_JARFILE=\"$SERVER_JARFILE.jar\"\r\nfi\r\n\r\n#Downloading jars\r\necho -e \"Downloading forge version ${FORGE_VERSION}\"\r\nif [ ! -z \"${DOWNLOAD_LINK}\" ]; then \r\n if curl --output \/dev\/null --silent --head --fail ${DOWNLOAD_LINK}-installer.jar; then\r\n echo -e \"installer jar download link is valid.\"\r\n else\r\n echo -e \"link is invalid closing out\"\r\n exit 2\r\n fi\r\n\r\n echo -e \"no download link closing out\"\r\n exit 3\r\nfi\r\n\r\ncurl -s -o installer.jar -sS ${DOWNLOAD_LINK}-installer.jar\r\n\r\n#Checking if downloaded jars exist\r\nif [ ! -f .\/installer.jar ]; then\r\n echo \"!!! Error by downloading forge version ${FORGE_VERSION} !!!\"\r\n exit\r\nfi\r\n\r\n#Installing server\r\necho -e \"Installing forge server.\\n\"\r\njava -jar installer.jar --installServer || { echo -e \"install failed\"; exit 4; }\r\n\r\nmv $FORGE_JAR $SERVER_JARFILE\r\n\r\n#Deleting installer.jar\r\necho -e \"Deleting installer.jar file.\\n\"\r\nrm -rf installer.jar", "script": "#!\/bin\/bash\r\n# Forge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napt update\r\napt install -y curl jq\r\n\r\n#Go into main direction\r\nif [ ! -d \/mnt\/server ]; then\r\n mkdir \/mnt\/server\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\nif [ ! -z ${FORGE_VERSION} ]; then\r\n DOWNLOAD_LINK=https:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/${FORGE_VERSION}\/forge-${FORGE_VERSION}\r\nelse\r\n JSON_DATA=$(curl -sSL https:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/promotions_slim.json)\r\n\r\n if [ \"${MC_VERSION}\" == \"latest\" ] || [ \"${MC_VERSION}\" == \"\" ] ; then\r\n echo -e \"getting latest recommended version of forge.\"\r\n MC_VERSION=$(echo -e ${JSON_DATA} | jq -r '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains(\"recommended\")) | split(\"-\")[0]' | sort -t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n | tail -1)\r\n \tBUILD_TYPE=recommended\r\n fi\r\n\r\n if [ \"${BUILD_TYPE}\" != \"recommended\" ] && [ \"${BUILD_TYPE}\" != \"latest\" ]; then\r\n BUILD_TYPE=recommended\r\n fi\r\n\r\n echo -e \"minecraft version: ${MC_VERSION}\"\r\n echo -e \"build type: ${BUILD_TYPE}\"\r\n\r\n ## some variables for getting versions and things\r\n FILE_SITE=$(echo -e ${JSON_DATA} | jq -r '.homepage' | sed \"s\/http:\/https:\/g\")\r\n VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION \"${MC_VERSION}\" --arg BUILD_TYPE \"${BUILD_TYPE}\" '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains($BUILD_TYPE))')\r\n\r\n ## locating the forge version\r\n if [ \"${VERSION_KEY}\" == \"\" ] && [ \"${BUILD_TYPE}\" == \"recommended\" ]; then\r\n echo -e \"dropping back to latest from recommended due to there not being a recommended version of forge for the mc version requested.\"\r\n VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION \"${MC_VERSION}\" '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains(\"recommended\"))')\r\n fi\r\n\r\n ## Error if the mc version set wasn't valid.\r\n if [ \"${VERSION_KEY}\" == \"\" ] || [ \"${VERSION_KEY}\" == \"null\" ]; then\r\n \techo -e \"The install failed because there is no valid version of forge for the version on minecraft selected.\"\r\n \texit 1\r\n fi\r\n\r\n FORGE_VERSION=$(echo -e ${JSON_DATA} | jq -r --arg VERSION_KEY \"$VERSION_KEY\" '.promos | .[$VERSION_KEY]')\r\n\r\n if [ \"${MC_VERSION}\" == \"1.7.10\" ] || [ \"${MC_VERSION}\" == \"1.8.9\" ]; then\r\n DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}\/forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}.jar\r\n if [ \"${MC_VERSION}\" == \"1.7.10\" ]; then\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}-universal.jar\r\n fi\r\n else\r\n DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}\/forge-${MC_VERSION}-${FORGE_VERSION}\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}.jar\r\n fi\r\nfi\r\n\r\n\r\n#Adding .jar when not eding by SERVER_JARFILE\r\nif [[ ! $SERVER_JARFILE = *\\.jar ]]; then\r\n SERVER_JARFILE=\"$SERVER_JARFILE.jar\"\r\nfi\r\n\r\n#Downloading jars\r\necho -e \"Downloading forge version ${FORGE_VERSION}\"\r\necho -e \"Download link is ${DOWNLOAD_LINK}\"\r\nif [ ! -z \"${DOWNLOAD_LINK}\" ]; then \r\n if curl --output \/dev\/null --silent --head --fail ${DOWNLOAD_LINK}-installer.jar; then\r\n echo -e \"installer jar download link is valid.\"\r\n else\r\n echo -e \"link is invalid closing out\"\r\n exit 2\r\n fi\r\nelse\r\n echo -e \"no download link closing out\"\r\n exit 3\r\nfi\r\n\r\ncurl -s -o installer.jar -sS ${DOWNLOAD_LINK}-installer.jar\r\n\r\n#Checking if downloaded jars exist\r\nif [ ! -f .\/installer.jar ]; then\r\n echo \"!!! Error by downloading forge version ${FORGE_VERSION} !!!\"\r\n exit\r\nfi\r\n\r\n#Installing server\r\necho -e \"Installing forge server.\\n\"\r\njava -jar installer.jar --installServer || { echo -e \"install failed\"; exit 4; }\r\n\r\nmv $FORGE_JAR $SERVER_JARFILE\r\n\r\n#Deleting installer.jar\r\necho -e \"Deleting installer.jar file.\\n\"\r\nrm -rf installer.jar",
"container": "openjdk:8-jdk-slim", "container": "openjdk:8-jdk-slim",
"entrypoint": "bash" "entrypoint": "bash"
} }
@ -28,8 +28,8 @@
"description": "The name of the Jarfile to use when running Forge Mod.", "description": "The name of the Jarfile to use when running Forge Mod.",
"env_variable": "SERVER_JARFILE", "env_variable": "SERVER_JARFILE",
"default_value": "server.jar", "default_value": "server.jar",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/" "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The version of minecraft you want to install for.\r\n\r\nLeaving latest will install the latest recommended version.", "description": "The version of minecraft you want to install for.\r\n\r\nLeaving latest will install the latest recommended version.",
"env_variable": "MC_VERSION", "env_variable": "MC_VERSION",
"default_value": "latest", "default_value": "latest",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|max:9" "rules": "required|string|max:9"
}, },
{ {
@ -46,8 +46,8 @@
"description": "The type of server jar to download from forge.\r\n\r\nValid types are \"recommended\" and \"latest\".", "description": "The type of server jar to download from forge.\r\n\r\nValid types are \"recommended\" and \"latest\".",
"env_variable": "BUILD_TYPE", "env_variable": "BUILD_TYPE",
"default_value": "recommended", "default_value": "recommended",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|max:20" "rules": "required|string|max:20"
}, },
{ {
@ -55,8 +55,8 @@
"description": "Gets an exact version.\r\n\r\nEx. 1.15.2-31.2.4\r\n\r\nOverrides MC_VERSION and BUILD_TYPE. If it fails to download the server files it will fail to install.", "description": "Gets an exact version.\r\n\r\nEx. 1.15.2-31.2.4\r\n\r\nOverrides MC_VERSION and BUILD_TYPE. If it fails to download the server files it will fail to install.",
"env_variable": "FORGE_VERSION", "env_variable": "FORGE_VERSION",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|max:20" "rules": "required|string|max:20"
} }
] ]

View file

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2019-08-01T04:49:37-04:00", "exported_at": "2020-10-25T21:03:55+00:00",
"name": "Paper", "name": "Paper",
"author": "parker@pterodactyl.io", "author": "parker@pterodactyl.io",
"description": "High performance Spigot fork that aims to fix gameplay and mechanics inconsistencies.", "description": "High performance Spigot fork that aims to fix gameplay and mechanics inconsistencies.",
@ -17,9 +17,9 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/ash\r\n# Paper Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napk add --no-cache --update curl jq\r\n\r\nif [ -n \"${DL_PATH}\" ]; then\r\n echo -e \"using supplied download url\"\r\n DOWNLOAD_URL=`eval echo $(echo ${DL_PATH} | sed -e 's\/{{\/${\/g' -e 's\/}}\/}\/g')`\r\nelse\r\n VER_EXISTS=`curl -s https:\/\/papermc.io\/api\/v1\/paper | jq -r --arg VERSION $MINECRAFT_VERSION '.versions[] | IN($VERSION)' | grep true`\r\n LATEST_PAPER_VERSION=`curl -s https:\/\/papermc.io\/api\/v1\/paper | jq -r '.versions' | jq -r '.[0]'`\r\n \r\n if [ \"${VER_EXISTS}\" == \"true\" ]; then\r\n echo -e \"Version is valid. Using version ${MINECRAFT_VERSION}\"\r\n else\r\n echo -e \"Using the latest paper version\"\r\n MINECRAFT_VERSION=${LATEST_PAPER_VERSION}\r\n fi\r\n \r\n BUILD_EXISTS=`curl -s https:\/\/papermc.io\/api\/v1\/paper\/${MINECRAFT_VERSION} | jq -r --arg BUILD ${BUILD_NUMBER} '.builds.all[] | IN($BUILD)' | grep true`\r\n LATEST_PAPER_BUILD=`curl -s https:\/\/papermc.io\/api\/v1\/paper\/${MINECRAFT_VERSION} | jq -r '.builds.latest'`\r\n \r\n if [ \"${BUILD_EXISTS}\" == \"true\" ] || [ ${BUILD_NUMBER} == \"latest\" ]; then\r\n echo -e \"Build is valid. Using version ${BUILD_NUMBER}\"\r\n else\r\n echo -e \"Using the latest paper build\"\r\n BUILD_NUMBER=${LATEST_PAPER_BUILD}\r\n fi\r\n \r\n echo \"Version being downloaded\"\r\n echo -e \"MC Version: ${MINECRAFT_VERSION}\"\r\n echo -e \"Build: ${BUILD_NUMBER}\"\r\n DOWNLOAD_URL=https:\/\/papermc.io\/api\/v1\/paper\/${MINECRAFT_VERSION}\/${BUILD_NUMBER}\/download \r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\necho -e \"running curl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\"\r\n\r\nif [ -f ${SERVER_JARFILE} ]; then\r\n mv ${SERVER_JARFILE} ${SERVER_JARFILE}.old\r\nfi\r\n\r\ncurl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\r\n\r\nif [ ! -f server.properties ]; then\r\n echo -e \"Downloading MC server.properties\"\r\n curl -o server.properties https:\/\/raw.githubusercontent.com\/parkervcp\/eggs\/master\/minecraft_java\/server.properties\r\nfi", "script": "#!\/bin\/bash\r\n# Paper Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napt update\r\napt install -y curl jq\r\n\r\nif [ -n \"${DL_PATH}\" ]; then\r\n echo -e \"using supplied download url\"\r\n DOWNLOAD_URL=`eval echo $(echo ${DL_PATH} | sed -e 's\/{{\/${\/g' -e 's\/}}\/}\/g')`\r\nelse\r\n VER_EXISTS=`curl -s https:\/\/papermc.io\/api\/v1\/paper | jq -r --arg VERSION $MINECRAFT_VERSION '.versions[] | contains($VERSION)' | grep true`\r\n LATEST_PAPER_VERSION=`curl -s https:\/\/papermc.io\/api\/v1\/paper | jq -r '.versions' | jq -r '.[0]'`\r\n \r\n if [ \"${VER_EXISTS}\" == \"true\" ]; then\r\n echo -e \"Version is valid. Using version ${MINECRAFT_VERSION}\"\r\n else\r\n echo -e \"Using the latest paper version\"\r\n MINECRAFT_VERSION=${LATEST_PAPER_VERSION}\r\n fi\r\n \r\n BUILD_EXISTS=`curl -s https:\/\/papermc.io\/api\/v1\/paper\/${MINECRAFT_VERSION} | jq -r --arg BUILD ${BUILD_NUMBER} '.builds.all[] | IN($BUILD)' | grep true`\r\n LATEST_PAPER_BUILD=`curl -s https:\/\/papermc.io\/api\/v1\/paper\/${MINECRAFT_VERSION} | jq -r '.builds.latest'`\r\n \r\n if [ \"${BUILD_EXISTS}\" == \"true\" ] || [ ${BUILD_NUMBER} == \"latest\" ]; then\r\n echo -e \"Build is valid. Using version ${BUILD_NUMBER}\"\r\n else\r\n echo -e \"Using the latest paper build\"\r\n BUILD_NUMBER=${LATEST_PAPER_BUILD}\r\n fi\r\n \r\n echo \"Version being downloaded\"\r\n echo -e \"MC Version: ${MINECRAFT_VERSION}\"\r\n echo -e \"Build: ${BUILD_NUMBER}\"\r\n DOWNLOAD_URL=https:\/\/papermc.io\/api\/v1\/paper\/${MINECRAFT_VERSION}\/${BUILD_NUMBER}\/download \r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\necho -e \"running curl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\"\r\n\r\nif [ -f ${SERVER_JARFILE} ]; then\r\n mv ${SERVER_JARFILE} ${SERVER_JARFILE}.old\r\nfi\r\n\r\ncurl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\r\n\r\nif [ ! -f server.properties ]; then\r\n echo -e \"Downloading MC server.properties\"\r\n curl -o server.properties https:\/\/raw.githubusercontent.com\/parkervcp\/eggs\/master\/minecraft\/java\/server.properties\r\nfi",
"container": "alpine:3.9", "container": "debian:buster-slim",
"entrypoint": "ash" "entrypoint": "bash"
} }
}, },
"variables": [ "variables": [
@ -28,8 +28,8 @@
"description": "The version of minecraft to download. \r\n\r\nLeave at latest to always get the latest version. Invalid versions will default to latest.", "description": "The version of minecraft to download. \r\n\r\nLeave at latest to always get the latest version. Invalid versions will default to latest.",
"env_variable": "MINECRAFT_VERSION", "env_variable": "MINECRAFT_VERSION",
"default_value": "latest", "default_value": "latest",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "nullable|string|max:20" "rules": "nullable|string|max:20"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The name of the server jarfile to run the server with.", "description": "The name of the server jarfile to run the server with.",
"env_variable": "SERVER_JARFILE", "env_variable": "SERVER_JARFILE",
"default_value": "server.jar", "default_value": "server.jar",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|max:20" "rules": "required|string|max:20"
}, },
{ {
@ -46,8 +46,8 @@
"description": "A URL to use to download a server.jar rather than the ones in the install script. This is not user viewable.", "description": "A URL to use to download a server.jar rather than the ones in the install script. This is not user viewable.",
"env_variable": "DL_PATH", "env_variable": "DL_PATH",
"default_value": "", "default_value": "",
"user_viewable": 0, "user_viewable": false,
"user_editable": 0, "user_editable": false,
"rules": "nullable|string" "rules": "nullable|string"
}, },
{ {
@ -55,8 +55,8 @@
"description": "The build number for the paper release.\r\n\r\nLeave at latest to always get the latest version. Invalid versions will default to latest.", "description": "The build number for the paper release.\r\n\r\nLeave at latest to always get the latest version. Invalid versions will default to latest.",
"env_variable": "BUILD_NUMBER", "env_variable": "BUILD_NUMBER",
"default_value": "latest", "default_value": "latest",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|string|max:20" "rules": "required|string|max:20"
} }
] ]

View file

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2017-11-03T22:20:03-05:00", "exported_at": "2020-10-19T23:26:54+00:00",
"name": "Sponge (SpongeVanilla)", "name": "Sponge (SpongeVanilla)",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "SpongeVanilla is the SpongeAPI implementation for Vanilla Minecraft.", "description": "SpongeVanilla is the SpongeAPI implementation for Vanilla Minecraft.",
@ -17,9 +17,9 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/ash\n# Sponge Installation Script\n#\n# Server Files: \/mnt\/server\n\napk update\napk add curl\n\ncd \/mnt\/server\n\ncurl -sSL \"https:\/\/repo.spongepowered.org\/maven\/org\/spongepowered\/spongevanilla\/${SPONGE_VERSION}\/spongevanilla-${SPONGE_VERSION}.jar\" -o ${SERVER_JARFILE}", "script": "#!\/bin\/bash\r\n# Sponge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\napt update\r\napt install -y curl\r\n\r\ncd \/mnt\/server\r\n\r\ncurl -sSL \"https:\/\/repo.spongepowered.org\/maven\/org\/spongepowered\/spongevanilla\/${SPONGE_VERSION}\/spongevanilla-${SPONGE_VERSION}.jar\" -o ${SERVER_JARFILE}",
"container": "alpine:3.9", "container": "debian:buster-slim",
"entrypoint": "ash" "entrypoint": "bash"
} }
}, },
"variables": [ "variables": [
@ -28,8 +28,8 @@
"description": "The version of SpongeVanilla to download and use.", "description": "The version of SpongeVanilla to download and use.",
"env_variable": "SPONGE_VERSION", "env_variable": "SPONGE_VERSION",
"default_value": "1.11.2-6.1.0-BETA-21", "default_value": "1.11.2-6.1.0-BETA-21",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|regex:\/^([a-zA-Z0-9.\\-_]+)$\/" "rules": "required|regex:\/^([a-zA-Z0-9.\\-_]+)$\/"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The name of the Jarfile to use when running SpongeVanilla.", "description": "The name of the Jarfile to use when running SpongeVanilla.",
"env_variable": "SERVER_JARFILE", "env_variable": "SERVER_JARFILE",
"default_value": "server.jar", "default_value": "server.jar",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/" "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
} }
] ]

View file

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2019-12-25T19:55:01-05:00", "exported_at": "2020-10-23T23:04:17+00:00",
"name": "Vanilla Minecraft", "name": "Vanilla Minecraft",
"author": "support@pterodactyl.io", "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.", "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.",
@ -17,9 +17,9 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/ash\r\n# Vanilla MC Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napk add curl --no-cache --update jq\r\n\r\nmkdir -p \/mnt\/server\r\ncd \/mnt\/server\r\n\r\nLATEST_VERSION=`curl https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq -r '.latest.release'`\r\n\r\necho -e \"latest version is $LATEST_VERSION\"\r\n\r\nif [ -z \"$VANILLA_VERSION\" ] || [ \"$VANILLA_VERSION\" == \"latest\" ]; then\r\n MANIFEST_URL=$(curl -sSL https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq --arg VERSION $LATEST_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url')\r\nelse\r\n MANIFEST_URL=$(curl -sSL https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq --arg VERSION $VANILLA_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url')\r\nfi\r\n\r\nDOWNLOAD_URL=$(curl ${MANIFEST_URL} | jq .downloads.server | jq -r '. | .url')\r\n\r\necho -e \"running: curl -o ${SERVER_JARFILE} $DOWNLOAD_URL\"\r\ncurl -o ${SERVER_JARFILE} $DOWNLOAD_URL\r\n\r\necho -e \"Install Complete\"", "script": "#!\/bin\/bash\r\n# Vanilla MC Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napt update\r\napt install -y jq curl\r\n\r\nmkdir -p \/mnt\/server\r\ncd \/mnt\/server\r\n\r\nLATEST_VERSION=`curl https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq -r '.latest.release'`\r\n\r\necho -e \"latest version is $LATEST_VERSION\"\r\n\r\nif [ -z \"$VANILLA_VERSION\" ] || [ \"$VANILLA_VERSION\" == \"latest\" ]; then\r\n MANIFEST_URL=$(curl -sSL https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq --arg VERSION $LATEST_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url')\r\nelse\r\n MANIFEST_URL=$(curl -sSL https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq --arg VERSION $VANILLA_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url')\r\nfi\r\n\r\nDOWNLOAD_URL=$(curl ${MANIFEST_URL} | jq .downloads.server | jq -r '. | .url')\r\n\r\necho -e \"running: curl -o ${SERVER_JARFILE} $DOWNLOAD_URL\"\r\ncurl -o ${SERVER_JARFILE} $DOWNLOAD_URL\r\n\r\necho -e \"Install Complete\"",
"container": "alpine:3.10", "container": "debian:buster-slim",
"entrypoint": "ash" "entrypoint": "bash"
} }
}, },
"variables": [ "variables": [
@ -28,8 +28,8 @@
"description": "The name of the server jarfile to run the server with.", "description": "The name of the server jarfile to run the server with.",
"env_variable": "SERVER_JARFILE", "env_variable": "SERVER_JARFILE",
"default_value": "server.jar", "default_value": "server.jar",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/" "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The version of Minecraft Vanilla to install. Use \"latest\" to install the latest version.", "description": "The version of Minecraft Vanilla to install. Use \"latest\" to install the latest version.",
"env_variable": "VANILLA_VERSION", "env_variable": "VANILLA_VERSION",
"default_value": "latest", "default_value": "latest",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|between:3,15" "rules": "required|string|between:3,15"
} }
] ]

View file

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2018-01-21T16:58:36-06:00", "exported_at": "2020-10-20T00:03:09+00:00",
"name": "Rust", "name": "Rust",
"author": "support@pterodactyl.io", "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.", "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.",
@ -17,8 +17,8 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "apt update\r\napt -y --no-install-recommends install curl unzip lib32gcc1 ca-certificates\r\ncd \/tmp\r\ncurl -sSL -o steamcmd.tar.gz http:\/\/media.steampowered.com\/installer\/steamcmd_linux.tar.gz\r\n\r\nmkdir -p \/mnt\/server\/steam\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steam\r\ncd \/mnt\/server\/steam\r\nchown -R root:root \/mnt\r\n\r\nexport HOME=\/mnt\/server\r\n.\/steamcmd.sh +login anonymous +force_install_dir \/mnt\/server +app_update 258550 +quit\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v \/mnt\/server\/steam\/linux32\/steamclient.so \/mnt\/server\/.steam\/sdk32\/steamclient.so", "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\nSRCDS_APPID=258550\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\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} 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": "ubuntu:16.04", "container": "debian:buster-slim",
"entrypoint": "bash" "entrypoint": "bash"
} }
}, },
@ -28,8 +28,8 @@
"description": "The name of your server in the public server list.", "description": "The name of your server in the public server list.",
"env_variable": "HOSTNAME", "env_variable": "HOSTNAME",
"default_value": "A Rust Server", "default_value": "A Rust Server",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|max:40" "rules": "required|string|max:40"
}, },
{ {
@ -37,8 +37,8 @@
"description": "Set whether you want the server to use and auto update OxideMod or not. Valid options are \"1\" for true and \"0\" for false.", "description": "Set whether you want the server to use and auto update OxideMod or not. Valid options are \"1\" for true and \"0\" for false.",
"env_variable": "OXIDE", "env_variable": "OXIDE",
"default_value": "0", "default_value": "0",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|boolean" "rules": "required|boolean"
}, },
{ {
@ -46,8 +46,8 @@
"description": "The world file for Rust to use.", "description": "The world file for Rust to use.",
"env_variable": "LEVEL", "env_variable": "LEVEL",
"default_value": "Procedural Map", "default_value": "Procedural Map",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|max:20" "rules": "required|string|max:20"
}, },
{ {
@ -55,8 +55,8 @@
"description": "The description under your server title. Commonly used for rules & info. Use \\n for newlines.", "description": "The description under your server title. Commonly used for rules & info. Use \\n for newlines.",
"env_variable": "DESCRIPTION", "env_variable": "DESCRIPTION",
"default_value": "Powered by Pterodactyl", "default_value": "Powered by Pterodactyl",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string" "rules": "required|string"
}, },
{ {
@ -64,8 +64,8 @@
"description": "The URL for your server. This is what comes up when clicking the \"Visit Website\" button.", "description": "The URL for your server. This is what comes up when clicking the \"Visit Website\" button.",
"env_variable": "SERVER_URL", "env_variable": "SERVER_URL",
"default_value": "http:\/\/pterodactyl.io", "default_value": "http:\/\/pterodactyl.io",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "nullable|url" "rules": "nullable|url"
}, },
{ {
@ -73,8 +73,8 @@
"description": "The world size for a procedural map.", "description": "The world size for a procedural map.",
"env_variable": "WORLD_SIZE", "env_variable": "WORLD_SIZE",
"default_value": "3000", "default_value": "3000",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|integer" "rules": "required|integer"
}, },
{ {
@ -82,8 +82,8 @@
"description": "The seed for a procedural map.", "description": "The seed for a procedural map.",
"env_variable": "WORLD_SEED", "env_variable": "WORLD_SEED",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "nullable|string" "rules": "nullable|string"
}, },
{ {
@ -91,8 +91,8 @@
"description": "The maximum amount of players allowed in the server at once.", "description": "The maximum amount of players allowed in the server at once.",
"env_variable": "MAX_PLAYERS", "env_variable": "MAX_PLAYERS",
"default_value": "40", "default_value": "40",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|integer" "rules": "required|integer"
}, },
{ {
@ -100,8 +100,8 @@
"description": "The header image for the top of your server listing.", "description": "The header image for the top of your server listing.",
"env_variable": "SERVER_IMG", "env_variable": "SERVER_IMG",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "nullable|url" "rules": "nullable|url"
}, },
{ {
@ -109,8 +109,8 @@
"description": "Port for RCON connections.", "description": "Port for RCON connections.",
"env_variable": "RCON_PORT", "env_variable": "RCON_PORT",
"default_value": "28016", "default_value": "28016",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|integer" "rules": "required|integer"
}, },
{ {
@ -118,17 +118,17 @@
"description": "RCON access password.", "description": "RCON access password.",
"env_variable": "RCON_PASS", "env_variable": "RCON_PASS",
"default_value": "CHANGEME", "default_value": "CHANGEME",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|max:64" "rules": "required|string|max:64"
}, },
{ {
"name": "Save Interval", "name": "Save Interval",
"description": "Sets the servers auto-save interval in seconds.", "description": "Sets the server\u2019s auto-save interval in seconds.",
"env_variable": "SAVEINTERVAL", "env_variable": "SAVEINTERVAL",
"default_value": "60", "default_value": "60",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|integer" "rules": "required|integer"
}, },
{ {
@ -136,8 +136,8 @@
"description": "Add additional startup parameters to the server.", "description": "Add additional startup parameters to the server.",
"env_variable": "ADDITIONAL_ARGS", "env_variable": "ADDITIONAL_ARGS",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "nullable|string" "rules": "nullable|string"
} }
] ]

View file

@ -3,78 +3,87 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2018-10-29T20:51:32+01:00", "exported_at": "2020-10-19T23:29:06+00:00",
"name": "Ark: Survival Evolved", "name": "Ark: Survival Evolved",
"author": "support@pterodactyl.io", "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", "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",
"image": "quay.io\/pterodactyl\/core:source", "image": "quay.io\/parkervcp\/pterodactyl-images:debian_source",
"startup": "\"cd ShooterGame\/Binaries\/Linux && .\/ShooterGameServer {{SERVER_MAP}}?listen?SessionName='{{SESSION_NAME}}'?ServerPassword={{ARK_PASSWORD}}?ServerAdminPassword={{ARK_ADMIN_PASSWORD}}?Port={{PORT}}?MaxPlayers={{SERVER_MAX_PLAYERS}}?RCONPort={{RCON_PORT}}?QueryPort={{QUERY_PORT}}?RCONEnabled={{ENABLE_RCON}} -server -log\"", "startup": "rmv() { echo -e \"stoppping server\"; 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={{ENABLE_RCON}}$( [ \"$BATTLE_EYE\" == \"0\" ] || printf %s '?-NoBattlEye' ) -server -log & until echo \"waiting for rcon connection...\"; rcon -a 127.0.0.1:${RCON_PORT} -p ${ARK_ADMIN_PASSWORD}; do sleep 5; done",
"config": { "config": {
"files": "{}", "files": "{}",
"startup": "{\r\n \"done\": \"Setting breakpad minidump AppID = 346110\",\r\n \"userInteraction\": []\r\n}", "startup": "{\r\n \"done\": \"Waiting commands for 127.0.0.1:\",\r\n \"userInteraction\": []\r\n}",
"logs": "{\r\n \"custom\": true,\r\n \"location\": \"logs\/latest.log\"\r\n}", "logs": "{\r\n \"custom\": true,\r\n \"location\": \"logs\/latest.log\"\r\n}",
"stop": "^C" "stop": "^C"
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/bash\r\n# ARK: Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napt -y update\r\napt -y --no-install-recommends install curl lib32gcc1 ca-certificates\r\n\r\ncd \/tmp\r\ncurl -sSL -o steamcmd.tar.gz http:\/\/media.steampowered.com\/installer\/steamcmd_linux.tar.gz\r\n\r\nmkdir -p \/mnt\/server\/steamcmd\r\nmkdir -p \/mnt\/server\/Engine\/Binaries\/ThirdParty\/SteamCMD\/Linux\r\n\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/Engine\/Binaries\/ThirdParty\/SteamCMD\/Linux\r\n\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\n\r\nexport HOME=\/mnt\/server\r\n.\/steamcmd.sh +login anonymous +force_install_dir \/mnt\/server +app_update 376030 +quit\r\n\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\ncd \/mnt\/server\/Engine\/Binaries\/ThirdParty\/SteamCMD\/Linux\r\n\r\nln -sf ..\/..\/..\/..\/..\/Steam\/steamapps steamapps\r\n\r\ncd \/mnt\/server", "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 --no-install-suggests 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\n\r\nmkdir -p \/mnt\/server\/Engine\/Binaries\/ThirdParty\/SteamCMD\/Linux\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/Engine\/Binaries\/ThirdParty\/SteamCMD\/Linux\r\n\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\r\n\r\n## create a symbolic link for loading mods\r\ncd \/mnt\/server\/Engine\/Binaries\/ThirdParty\/SteamCMD\/Linux\r\nln -sf ..\/..\/..\/..\/..\/Steam\/steamapps steamapps\r\ncd \/mnt\/server",
"container": "ubuntu:16.04", "container": "debian:buster-slim",
"entrypoint": "bash" "entrypoint": "bash"
} }
}, },
"variables": [ "variables": [
{
"name": "Server Name",
"description": "ARK server name",
"env_variable": "SESSION_NAME",
"default_value": "ARK SERVER",
"user_viewable": 1,
"user_editable": 1,
"rules": "required|string|max:128"
},
{ {
"name": "Server Password", "name": "Server Password",
"description": "If specified, players must provide this password to join the server.", "description": "If specified, players must provide this password to join the server.",
"env_variable": "ARK_PASSWORD", "env_variable": "ARK_PASSWORD",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "nullable|alpha_dash|between:1,100" "rules": "nullable|alpha_dash|between:1,100"
}, },
{ {
"name": "Admin Password", "name": "Admin Password",
"description": "If specified, players must provide this password (via the in-game console) to gain access to administrator commands on the server.", "description": "If specified, players must provide this password (via the in-game console) to gain access to administrator commands on the server.",
"env_variable": "ARK_ADMIN_PASSWORD", "env_variable": "ARK_ADMIN_PASSWORD",
"default_value": "", "default_value": "PleaseChangeMe",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "nullable|alpha_dash|between:1,100" "rules": "nullable|alpha_dash|between:1,100"
}, },
{ {
"name": "Server Port", "name": "Server Map",
"description": "ARK server port used by client.", "description": "Available Maps: TheIsland, TheCenter, Ragnarok, ScorchedEarth_P, Aberration_P, Extinction, Valguero_P, Genesis",
"env_variable": "PORT", "env_variable": "SERVER_MAP",
"default_value": "7777", "default_value": "TheIsland",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|numeric" "rules": "required|string|max:20"
},
{
"name": "App ID",
"description": "ARK steam app id for auto updates. Leave blank to avoid auto update.",
"env_variable": "SRCDS_APPID",
"default_value": "376030",
"user_viewable": true,
"user_editable": false,
"rules": "nullable|numeric"
},
{
"name": "Server Name",
"description": "ARK server name",
"env_variable": "SESSION_NAME",
"default_value": "A Pterodactyl Hosted ARK Server",
"user_viewable": true,
"user_editable": true,
"rules": "required|string|max:128"
}, },
{ {
"name": "Use Rcon", "name": "Use Rcon",
"description": "Enable or disable rcon system. (true or false)", "description": "Enable or disable rcon system. (true or false)\r\n\r\nDefault True for the console to work.",
"env_variable": "ENABLE_RCON", "env_variable": "ENABLE_RCON",
"default_value": "false", "default_value": "True",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": false,
"rules": "required|string|max:5" "rules": "required|string|in:True,False"
}, },
{ {
"name": "Rcon Port", "name": "Rcon Port",
"description": "ARK rcon port used by rcon tools.", "description": "ARK rcon port used by rcon tools.",
"env_variable": "RCON_PORT", "env_variable": "RCON_PORT",
"default_value": "27020", "default_value": "27020",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|numeric" "rules": "required|numeric"
}, },
{ {
@ -82,36 +91,27 @@
"description": "ARK query port used by steam server browser and ark client server browser.", "description": "ARK query port used by steam server browser and ark client server browser.",
"env_variable": "QUERY_PORT", "env_variable": "QUERY_PORT",
"default_value": "27015", "default_value": "27015",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|numeric" "rules": "required|numeric"
}, },
{ {
"name": "Maximum Players", "name": "Auto-update server",
"description": "Specifies the maximum number of players that can play on the server simultaneously.", "description": "This is to enable auto-updating for servers.\r\n\r\nDefault is 0. Set to 1 to update",
"env_variable": "SERVER_MAX_PLAYERS", "env_variable": "AUTO_UPDATE",
"default_value": "20", "default_value": "0",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|numeric|digits_between:1,4" "rules": "required|boolean"
}, },
{ {
"name": "App ID", "name": "Ballte Eye",
"description": "ARK steam app id for auto updates. Leave blank to avoid auto update.", "description": "Enable BattleEye\r\n\r\n0 to disable\r\n1 to enable\r\n\r\ndefault=\"1\"",
"env_variable": "SRCDS_APPID", "env_variable": "BATTLE_EYE",
"default_value": "376030", "default_value": "1",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": true,
"rules": "nullable|numeric" "rules": "required|boolean"
},
{
"name": "Server Map",
"description": "Available Maps: TheIsland, TheCenter, Ragnarok, ScorchedEarth_P, Aberration_P, Extinction",
"env_variable": "SERVER_MAP",
"default_value": "TheIsland",
"user_viewable": 1,
"user_editable": 1,
"rules": "required|string|max:20"
} }
] ]
} }

View file

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2019-12-08T10:52:19-05:00", "exported_at": "2020-10-19T23:29:57+00:00",
"name": "Counter-Strike: Global Offensive", "name": "Counter-Strike: Global Offensive",
"author": "support@pterodactyl.io", "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.", "description": "Counter-Strike: Global Offensive is a multiplayer first-person shooter video game developed by Hidden Path Entertainment and Valve Corporation.",
@ -28,8 +28,8 @@
"description": "The default map for the server.", "description": "The default map for the server.",
"env_variable": "SRCDS_MAP", "env_variable": "SRCDS_MAP",
"default_value": "de_dust2", "default_value": "de_dust2",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|alpha_dash" "rules": "required|string|alpha_dash"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The Steam Account Token required for the server to be displayed publicly.", "description": "The Steam Account Token required for the server to be displayed publicly.",
"env_variable": "STEAM_ACC", "env_variable": "STEAM_ACC",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|alpha_num|size:32" "rules": "required|string|alpha_num|size:32"
}, },
{ {
@ -46,8 +46,8 @@
"description": "Required for game to update on server restart. Do not modify this.", "description": "Required for game to update on server restart. Do not modify this.",
"env_variable": "SRCDS_APPID", "env_variable": "SRCDS_APPID",
"default_value": "740", "default_value": "740",
"user_viewable": 0, "user_viewable": false,
"user_editable": 0, "user_editable": false,
"rules": "required|string|max:20" "rules": "required|string|max:20"
} }
] ]

View file

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2019-12-08T10:54:26-05:00", "exported_at": "2020-10-19T23:33:52+00:00",
"name": "Custom Source Engine Game", "name": "Custom Source Engine Game",
"author": "support@pterodactyl.io", "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.", "description": "This option allows modifying the startup arguments and other details to run a custom SRCDS based game on the panel.",
@ -17,8 +17,8 @@
}, },
"scripts": { "scripts": {
"installation": { "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\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# 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\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": "ubuntu:18.04", "container": "debian:buster-slim",
"entrypoint": "bash" "entrypoint": "bash"
} }
}, },
@ -28,8 +28,8 @@
"description": "The ID corresponding to the game to download and run using SRCDS.", "description": "The ID corresponding to the game to download and run using SRCDS.",
"env_variable": "SRCDS_APPID", "env_variable": "SRCDS_APPID",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|numeric|digits_between:1,6" "rules": "required|numeric|digits_between:1,6"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The name corresponding to the game to download and run using SRCDS.", "description": "The name corresponding to the game to download and run using SRCDS.",
"env_variable": "SRCDS_GAME", "env_variable": "SRCDS_GAME",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|alpha_dash|between:1,100" "rules": "required|alpha_dash|between:1,100"
}, },
{ {
@ -46,8 +46,8 @@
"description": "The default map for the server.", "description": "The default map for the server.",
"env_variable": "SRCDS_MAP", "env_variable": "SRCDS_MAP",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|alpha_dash" "rules": "required|string|alpha_dash"
} }
] ]

View file

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2019-12-08T10:56:42-05:00", "exported_at": "2020-10-19T23:34:44+00:00",
"name": "Garrys Mod", "name": "Garrys Mod",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "Garrys Mod, is a sandbox physics game created by Garry Newman, and developed by his company, Facepunch Studios.", "description": "Garrys Mod, is a sandbox physics game created by Garry Newman, and developed by his company, Facepunch Studios.",
@ -17,8 +17,8 @@
}, },
"scripts": { "scripts": {
"installation": { "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\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\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# 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\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",
"container": "ubuntu:18.04", "container": "debian:buster-slim",
"entrypoint": "bash" "entrypoint": "bash"
} }
}, },
@ -28,8 +28,8 @@
"description": "The default map for the server.", "description": "The default map for the server.",
"env_variable": "SRCDS_MAP", "env_variable": "SRCDS_MAP",
"default_value": "gm_flatgrass", "default_value": "gm_flatgrass",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|alpha_dash" "rules": "required|string|alpha_dash"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The Steam Account Token required for the server to be displayed publicly.", "description": "The Steam Account Token required for the server to be displayed publicly.",
"env_variable": "STEAM_ACC", "env_variable": "STEAM_ACC",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "nullable|string|alpha_num|size:32" "rules": "nullable|string|alpha_num|size:32"
}, },
{ {
@ -46,8 +46,8 @@
"description": "Required for game to update on server restart. Do not modify this.", "description": "Required for game to update on server restart. Do not modify this.",
"env_variable": "SRCDS_APPID", "env_variable": "SRCDS_APPID",
"default_value": "4020", "default_value": "4020",
"user_viewable": 0, "user_viewable": false,
"user_editable": 0, "user_editable": false,
"rules": "required|string|max:20" "rules": "required|string|max:20"
}, },
{ {
@ -55,8 +55,8 @@
"description": "The ID of your workshop collection (the numbers at the end of the URL)", "description": "The ID of your workshop collection (the numbers at the end of the URL)",
"env_variable": "WORKSHOP_ID", "env_variable": "WORKSHOP_ID",
"default_value": "", "default_value": "",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "nullable|integer" "rules": "nullable|integer"
}, },
{ {
@ -64,8 +64,8 @@
"description": "The gamemode of your server.", "description": "The gamemode of your server.",
"env_variable": "GAMEMODE", "env_variable": "GAMEMODE",
"default_value": "sandbox", "default_value": "sandbox",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string" "rules": "required|string"
}, },
{ {
@ -73,8 +73,8 @@
"description": "The maximum amount of players allowed on your game server.", "description": "The maximum amount of players allowed on your game server.",
"env_variable": "MAX_PLAYERS", "env_variable": "MAX_PLAYERS",
"default_value": "32", "default_value": "32",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|integer|max:128" "rules": "required|integer|max:128"
}, },
{ {
@ -82,8 +82,8 @@
"description": "The tickrate defines how fast the server will update each entities location.", "description": "The tickrate defines how fast the server will update each entities location.",
"env_variable": "TICKRATE", "env_variable": "TICKRATE",
"default_value": "22", "default_value": "22",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|integer|max:100" "rules": "required|integer|max:100"
} }
] ]

View file

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2019-12-08T10:57:32-05:00", "exported_at": "2020-10-19T23:35:42+00:00",
"name": "Insurgency", "name": "Insurgency",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "Take to the streets for intense close quarters combat, where a team's survival depends upon securing crucial strongholds and destroying enemy supply in this multiplayer and cooperative Source Engine based experience.", "description": "Take to the streets for intense close quarters combat, where a team's survival depends upon securing crucial strongholds and destroying enemy supply in this multiplayer and cooperative Source Engine based experience.",
@ -17,8 +17,8 @@
}, },
"scripts": { "scripts": {
"installation": { "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\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# 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\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": "ubuntu:18.04", "container": "debian:buster-slim",
"entrypoint": "bash" "entrypoint": "bash"
} }
}, },
@ -28,8 +28,8 @@
"description": "The ID corresponding to the game to download and run using SRCDS.", "description": "The ID corresponding to the game to download and run using SRCDS.",
"env_variable": "SRCDS_APPID", "env_variable": "SRCDS_APPID",
"default_value": "237410", "default_value": "237410",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|regex:\/^(237410)$\/" "rules": "required|regex:\/^(237410)$\/"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The name corresponding to the game to download and run using SRCDS.", "description": "The name corresponding to the game to download and run using SRCDS.",
"env_variable": "SRCDS_GAME", "env_variable": "SRCDS_GAME",
"default_value": "insurgency", "default_value": "insurgency",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|regex:\/^(insurgency)$\/" "rules": "required|regex:\/^(insurgency)$\/"
}, },
{ {
@ -46,8 +46,8 @@
"description": "The default map to use when starting the server.", "description": "The default map to use when starting the server.",
"env_variable": "SRCDS_MAP", "env_variable": "SRCDS_MAP",
"default_value": "sinjar", "default_value": "sinjar",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|regex:\/^(\\w{1,20})$\/" "rules": "required|regex:\/^(\\w{1,20})$\/"
} }
] ]

View file

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2019-12-08T10:58:48-05:00", "exported_at": "2020-10-19T23:36:44+00:00",
"name": "Team Fortress 2", "name": "Team Fortress 2",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "Team Fortress 2 is a team-based first-person shooter multiplayer video game developed and published by Valve Corporation. It is the sequel to the 1996 mod Team Fortress for Quake and its 1999 remake.", "description": "Team Fortress 2 is a team-based first-person shooter multiplayer video game developed and published by Valve Corporation. It is the sequel to the 1996 mod Team Fortress for Quake and its 1999 remake.",
@ -17,8 +17,8 @@
}, },
"scripts": { "scripts": {
"installation": { "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\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# 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\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": "ubuntu:18.04", "container": "debian:buster-slim",
"entrypoint": "bash" "entrypoint": "bash"
} }
}, },
@ -28,8 +28,8 @@
"description": "The ID corresponding to the game to download and run using SRCDS.", "description": "The ID corresponding to the game to download and run using SRCDS.",
"env_variable": "SRCDS_APPID", "env_variable": "SRCDS_APPID",
"default_value": "232250", "default_value": "232250",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|regex:\/^(232250)$\/" "rules": "required|regex:\/^(232250)$\/"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The name corresponding to the game to download and run using SRCDS.", "description": "The name corresponding to the game to download and run using SRCDS.",
"env_variable": "SRCDS_GAME", "env_variable": "SRCDS_GAME",
"default_value": "tf", "default_value": "tf",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|regex:\/^(tf)$\/" "rules": "required|regex:\/^(tf)$\/"
}, },
{ {
@ -46,8 +46,8 @@
"description": "The default map to use when starting the server.", "description": "The default map to use when starting the server.",
"env_variable": "SRCDS_MAP", "env_variable": "SRCDS_MAP",
"default_value": "cp_dustbowl", "default_value": "cp_dustbowl",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|regex:\/^(\\w{1,20})$\/" "rules": "required|regex:\/^(\\w{1,20})$\/"
} }
] ]

View file

@ -1,45 +0,0 @@
{
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO",
"meta": {
"version": "PTDL_v1"
},
"exported_at": "2018-01-21T17:01:31-06:00",
"name": "Terraria Server (TShock)",
"author": "support@pterodactyl.io",
"description": "TShock is a server modification for Terraria, written in C#, and based upon the Terraria Server API. It uses JSON for configuration management, and offers several features not present in the Terraria Server normally.",
"image": "quay.io\/pterodactyl\/core:mono",
"startup": null,
"config": {
"files": "{\"tshock\/config.json\":{\"parser\": \"json\", \"find\":{\"ServerPort\": \"{{server.build.default.port}}\", \"MaxSlots\": \"{{server.build.env.MAX_SLOTS}}\"}}}",
"startup": "{\"done\": \"Type 'help' for a list of commands\", \"userInteraction\": []}",
"logs": "{\"custom\": false, \"location\": \"ServerLog.txt\"}",
"stop": "exit"
},
"scripts": {
"installation": {
"script": "#!\/bin\/ash\n# TShock Installation Script\n#\n# Server Files: \/mnt\/server\napk update\napk add curl unzip\n\ncd \/tmp\n\ncurl -sSLO https:\/\/github.com\/NyxStudios\/TShock\/releases\/download\/v${T_VERSION}\/tshock_${T_VERSION}.zip\n\nunzip -o tshock_${T_VERSION}.zip -d \/mnt\/server",
"container": "alpine:3.9",
"entrypoint": "ash"
}
},
"variables": [
{
"name": "TShock Version",
"description": "Which version of TShock to install and use.",
"env_variable": "T_VERSION",
"default_value": "4.3.22",
"user_viewable": 1,
"user_editable": 1,
"rules": "required|regex:\/^([0-9_\\.-]{5,10})$\/"
},
{
"name": "Maximum Slots",
"description": "Total number of slots to allow on the server.",
"env_variable": "MAX_SLOTS",
"default_value": "20",
"user_viewable": 1,
"user_editable": 0,
"rules": "required|numeric|digits_between:1,3"
}
]
}

View file

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2018-01-21T17:01:44-06:00", "exported_at": "2020-10-25T22:34:06+00:00",
"name": "Mumble Server", "name": "Mumble Server",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "Mumble is an open source, low-latency, high quality voice chat software primarily intended for use while gaming.", "description": "Mumble is an open source, low-latency, high quality voice chat software primarily intended for use while gaming.",
@ -17,9 +17,9 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/ash\n# Mumble Installation Script\n#\n# Server Files: \/mnt\/server\napk update\napk add tar curl\n\ncd \/tmp\n\ncurl -sSLO https:\/\/github.com\/mumble-voip\/mumble\/releases\/download\/${MUMBLE_VERSION}\/murmur-static_x86-${MUMBLE_VERSION}.tar.bz2\n\ntar -xjvf murmur-static_x86-${MUMBLE_VERSION}.tar.bz2\ncp -r murmur-static_x86-${MUMBLE_VERSION}\/* \/mnt\/server", "script": "#!\/bin\/bash\r\n# Mumble Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\nGITHUB_PACKAGE=mumble-voip\/mumble\r\nMATCH=murmur-static\r\n\r\napt update\r\napt install -y tar curl jq bzip2\r\n\r\nif [ ! -d \/mnt\/server\/ ]; then\r\n mkdir \/mnt\/server\/\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\nif [ -z \"${GITHUB_USER}\" ] && [ -z \"${GITHUB_OAUTH_TOKEN}\" ] ; then\r\n echo -e \"using anon api call\"\r\nelse\r\n echo -e \"user and oauth token set\"\r\n alias curl='curl -u ${GITHUB_USER}:${GITHUB_OAUTH_TOKEN} '\r\nfi\r\n\r\n## get release info and download links\r\nLATEST_JSON=$(curl --silent \"https:\/\/api.github.com\/repos\/${GITHUB_PACKAGE}\/releases\/latest\")\r\nRELEASES=$(curl --silent \"https:\/\/api.github.com\/repos\/${GITHUB_PACKAGE}\/releases\")\r\n\r\nif [ -z \"${VERSION}\" ] || [ \"${VERSION}\" == \"latest\" ]; then\r\n DOWNLOAD_LINK=$(echo ${LATEST_JSON} | jq .assets | jq -r .[].browser_download_url | grep -m 1 -i ${MATCH})\r\nelse\r\n VERSION_CHECK=$(echo ${RELEASES} | jq -r --arg VERSION \"${VERSION}\" '.[] | select(.tag_name==$VERSION) | .tag_name')\r\n if [ \"${VERSION}\" == \"${VERSION_CHECK}\" ]; then\r\n DOWNLOAD_LINK=$(echo ${RELEASES} | jq -r --arg VERSION \"${VERSION}\" '.[] | select(.tag_name==$VERSION) | .assets[].browser_download_url' | grep -m 1 -i ${MATCH})\r\n else\r\n echo -e \"defaulting to latest release\"\r\n DOWNLOAD_LINK=$(echo ${LATEST_JSON} | jq .assets | jq -r .[].browser_download_url)\r\n fi\r\nfi\r\n\r\ncurl -L ${DOWNLOAD_LINK} | tar xjv --strip-components=1",
"container": "alpine:3.9", "container": "debian:buster-slim",
"entrypoint": "ash" "entrypoint": "bash"
} }
}, },
"variables": [ "variables": [
@ -28,17 +28,17 @@
"description": "Maximum concurrent users on the mumble server.", "description": "Maximum concurrent users on the mumble server.",
"env_variable": "MAX_USERS", "env_variable": "MAX_USERS",
"default_value": "100", "default_value": "100",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|numeric|digits_between:1,5" "rules": "required|numeric|digits_between:1,5"
}, },
{ {
"name": "Server Version", "name": "Server Version",
"description": "Version of Mumble Server to download and use.", "description": "Version of Mumble Server to download and use.",
"env_variable": "MUMBLE_VERSION", "env_variable": "MUMBLE_VERSION",
"default_value": "1.3.1", "default_value": "latest",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|regex:\/^([0-9_\\.-]{5,8})$\/" "rules": "required|regex:\/^([0-9_\\.-]{5,8})$\/"
} }
] ]

View file

@ -3,23 +3,23 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2019-07-05T11:59:29-04:00", "exported_at": "2020-10-25T22:24:15+00:00",
"name": "Teamspeak3 Server", "name": "Teamspeak3 Server",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "VoIP software designed with security in mind, featuring crystal clear voice quality, endless customization options, and scalabilty up to thousands of simultaneous users.", "description": "VoIP software designed with security in mind, featuring crystal clear voice quality, endless customization options, and scalabilty up to thousands of simultaneous users.",
"image": "quay.io/parkervcp/pterodactyl-images:base_debian", "image": "quay.io\/parkervcp\/pterodactyl-images:base_debian",
"startup": "./ts3server default_voice_port={{SERVER_PORT}} query_port={{SERVER_PORT}} filetransfer_ip=0.0.0.0 filetransfer_port={{FILE_TRANSFER}} license_accepted=1", "startup": ".\/ts3server default_voice_port={{SERVER_PORT}} query_port={{SERVER_PORT}} filetransfer_ip=0.0.0.0 filetransfer_port={{FILE_TRANSFER}} license_accepted=1",
"config": { "config": {
"files": "{}", "files": "{}",
"startup": "{\r\n \"done\": \"listening on 0.0.0.0:\",\r\n \"userInteraction\": []\r\n}", "startup": "{\r\n \"done\": \"listening on 0.0.0.0:\",\r\n \"userInteraction\": []\r\n}",
"logs": "{\r\n \"custom\": true,\r\n \"location\": \"logs/ts3.log\"\r\n}", "logs": "{\r\n \"custom\": true,\r\n \"location\": \"logs\/ts3.log\"\r\n}",
"stop": "^C" "stop": "^C"
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!/bin/ash\r\n# TS3 Installation Script\r\n#\r\n# Server Files: /mnt/server\r\napk add --no-cache tar curl jq\r\n\r\nif [ -z ${TS_VERSION} ] || [ ${TS_VERSION} == latest ]; then\r\n TS_VERSION=$(wget https://teamspeak.com/versions/server.json -qO - | jq -r '.linux.x86_64.version')\r\nfi\r\n\r\ncd /mnt/server\r\n\r\n\r\necho -e \"getting files from http://files.teamspeak-services.com/releases/server/${TS_VERSION}/teamspeak3-server_linux_amd64-${TS_VERSION}.tar.bz2\"\r\ncurl http://files.teamspeak-services.com/releases/server/${TS_VERSION}/teamspeak3-server_linux_amd64-${TS_VERSION}.tar.bz2 | tar xj --strip-components=1", "script": "#!\/bin\/bash\r\n# TS3 Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napt update\r\napt install -y tar curl jq bzip2\r\n\r\nif [ -z ${TS_VERSION} ] || [ ${TS_VERSION} == latest ]; then\r\n TS_VERSION=$(curl -sSL https:\/\/teamspeak.com\/versions\/server.json | jq -r '.linux.x86_64.version')\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\necho -e \"getting files from http:\/\/files.teamspeak-services.com\/releases\/server\/${TS_VERSION}\/teamspeak3-server_linux_amd64-${TS_VERSION}.tar.bz2\" \r\ncurl -L http:\/\/files.teamspeak-services.com\/releases\/server\/${TS_VERSION}\/teamspeak3-server_linux_amd64-${TS_VERSION}.tar.bz2 | tar -xvj --strip-components=1\r\n\r\nrm teamspeak3-server_linux_amd64-${TS_VERSION}.tar.bz2",
"container": "alpine:3.9", "container": "debian:buster-slim",
"entrypoint": "ash" "entrypoint": "bash"
} }
}, },
"variables": [ "variables": [
@ -28,8 +28,8 @@
"description": "The version of Teamspeak 3 to use when running the server.", "description": "The version of Teamspeak 3 to use when running the server.",
"env_variable": "TS_VERSION", "env_variable": "TS_VERSION",
"default_value": "latest", "default_value": "latest",
"user_viewable": 1, "user_viewable": true,
"user_editable": 1, "user_editable": true,
"rules": "required|string|max:6" "rules": "required|string|max:6"
}, },
{ {
@ -37,8 +37,8 @@
"description": "The Teamspeak file transfer port", "description": "The Teamspeak file transfer port",
"env_variable": "FILE_TRANSFER", "env_variable": "FILE_TRANSFER",
"default_value": "30033", "default_value": "30033",
"user_viewable": 1, "user_viewable": true,
"user_editable": 0, "user_editable": false,
"rules": "required|integer|between:1,65535" "rules": "required|integer|between:1,65535"
} }
] ]

View file

@ -4,6 +4,7 @@
"@fortawesome/fontawesome-svg-core": "^1.2.32", "@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-solid-svg-icons": "^5.15.1", "@fortawesome/free-solid-svg-icons": "^5.15.1",
"@fortawesome/react-fontawesome": "^0.1.11", "@fortawesome/react-fontawesome": "^0.1.11",
"@types/react-copy-to-clipboard": "^4.3.0",
"axios": "^0.19.2", "axios": "^0.19.2",
"chart.js": "^2.8.0", "chart.js": "^2.8.0",
"codemirror": "^5.57.0", "codemirror": "^5.57.0",
@ -21,6 +22,7 @@
"path": "^0.12.7", "path": "^0.12.7",
"query-string": "^6.7.0", "query-string": "^6.7.0",
"react": "^16.13.1", "react": "^16.13.1",
"react-copy-to-clipboard": "^5.0.2",
"react-dom": "npm:@hot-loader/react-dom", "react-dom": "npm:@hot-loader/react-dom",
"react-fast-compare": "^3.2.0", "react-fast-compare": "^3.2.0",
"react-ga": "^3.1.2", "react-ga": "^3.1.2",

View file

@ -1,18 +1,10 @@
import http from '@/api/http'; import http from '@/api/http';
export default (uuid: string, file: string, content: string): Promise<void> => { export default async (uuid: string, file: string, content: string): Promise<void> => {
return new Promise((resolve, reject) => { await http.post(`/api/client/servers/${uuid}/files/write`, content, {
http.post(
`/api/client/servers/${uuid}/files/write`,
content,
{
params: { file }, params: { file },
headers: { headers: {
'Content-Type': 'text/plain', 'Content-Type': 'text/plain',
}, },
},
)
.then(() => resolve())
.catch(reject);
}); });
}; };

View file

@ -1,7 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Link, RouteComponentProps } from 'react-router-dom'; import { Link, RouteComponentProps } from 'react-router-dom';
import loginCheckpoint from '@/api/auth/loginCheckpoint'; import loginCheckpoint from '@/api/auth/loginCheckpoint';
import { httpErrorToHuman } from '@/api/http';
import LoginFormContainer from '@/components/auth/LoginFormContainer'; import LoginFormContainer from '@/components/auth/LoginFormContainer';
import { ActionCreator } from 'easy-peasy'; import { ActionCreator } from 'easy-peasy';
import { StaticContext } from 'react-router'; import { StaticContext } from 'react-router';
@ -20,8 +19,7 @@ interface Values {
type OwnProps = RouteComponentProps<Record<string, string | undefined>, StaticContext, { token?: string }> type OwnProps = RouteComponentProps<Record<string, string | undefined>, StaticContext, { token?: string }>
type Props = OwnProps & { type Props = OwnProps & {
addError: ActionCreator<FlashStore['addError']['payload']>; clearAndAddHttpError: ActionCreator<FlashStore['clearAndAddHttpError']['payload']>;
clearFlashes: ActionCreator<FlashStore['clearFlashes']['payload']>;
} }
const LoginCheckpointContainer = () => { const LoginCheckpointContainer = () => {
@ -79,9 +77,7 @@ const LoginCheckpointContainer = () => {
}; };
const EnhancedForm = withFormik<Props, Values>({ const EnhancedForm = withFormik<Props, Values>({
handleSubmit: ({ code, recoveryCode }, { setSubmitting, props: { addError, clearFlashes, location } }) => { handleSubmit: ({ code, recoveryCode }, { setSubmitting, props: { clearAndAddHttpError, location } }) => {
clearFlashes();
loginCheckpoint(location.state?.token || '', code, recoveryCode) loginCheckpoint(location.state?.token || '', code, recoveryCode)
.then(response => { .then(response => {
if (response.complete) { if (response.complete) {
@ -95,7 +91,7 @@ const EnhancedForm = withFormik<Props, Values>({
.catch(error => { .catch(error => {
console.error(error); console.error(error);
setSubmitting(false); setSubmitting(false);
addError({ message: httpErrorToHuman(error) }); clearAndAddHttpError({ error });
}); });
}, },
@ -106,7 +102,7 @@ const EnhancedForm = withFormik<Props, Values>({
})(LoginCheckpointContainer); })(LoginCheckpointContainer);
export default ({ history, location, ...props }: OwnProps) => { export default ({ history, location, ...props }: OwnProps) => {
const { addError, clearFlashes } = useFlash(); const { clearAndAddHttpError } = useFlash();
if (!location.state?.token) { if (!location.state?.token) {
history.replace('/auth/login'); history.replace('/auth/login');
@ -115,8 +111,7 @@ export default ({ history, location, ...props }: OwnProps) => {
} }
return <EnhancedForm return <EnhancedForm
addError={addError} clearAndAddHttpError={clearAndAddHttpError}
clearFlashes={clearFlashes}
history={history} history={history}
location={location} location={location}
{...props} {...props}

View file

@ -41,20 +41,22 @@ const StatusIndicatorBox = styled(GreyRowBox)<{ $status: ServerPowerState | unde
export default ({ server, className }: { server: Server; className?: string }) => { export default ({ server, className }: { server: Server; className?: string }) => {
const interval = useRef<number>(null); const interval = useRef<number>(null);
const [ isSuspended, setIsSuspended ] = useState(server.isSuspended);
const [ stats, setStats ] = useState<ServerStats | null>(null); const [ stats, setStats ] = useState<ServerStats | null>(null);
const [ statsError, setStatsError ] = useState(false);
const getStats = () => { const getStats = () => getServerResourceUsage(server.uuid)
setStatsError(false);
return getServerResourceUsage(server.uuid)
.then(data => setStats(data)) .then(data => setStats(data))
.catch(error => { .catch(error => console.error(error));
setStatsError(true);
console.error(error);
});
};
useEffect(() => { useEffect(() => {
setIsSuspended(stats?.isSuspended || server.isSuspended);
}, [ stats?.isSuspended, server.isSuspended ]);
useEffect(() => {
// Don't waste a HTTP request if there is nothing important to show to the user because
// the server is suspended.
if (isSuspended) return;
getStats().then(() => { getStats().then(() => {
// @ts-ignore // @ts-ignore
interval.current = setInterval(() => getStats(), 20000); interval.current = setInterval(() => getStats(), 20000);
@ -63,7 +65,7 @@ export default ({ server, className }: { server: Server; className?: string }) =
return () => { return () => {
interval.current && clearInterval(interval.current); interval.current && clearInterval(interval.current);
}; };
}, []); }, [ isSuspended ]);
const alarms = { cpu: false, memory: false, disk: false }; const alarms = { cpu: false, memory: false, disk: false };
if (stats) { if (stats) {
@ -101,9 +103,13 @@ export default ({ server, className }: { server: Server; className?: string }) =
</p> </p>
</div> </div>
<div css={tw`hidden col-span-7 lg:col-span-4 sm:flex items-baseline justify-center`}> <div css={tw`hidden col-span-7 lg:col-span-4 sm:flex items-baseline justify-center`}>
{!stats ? {(!stats || isSuspended) ?
!statsError ? isSuspended ?
<Spinner size={'small'}/> <div css={tw`flex-1 text-center`}>
<span css={tw`bg-red-500 rounded px-2 py-1 text-red-100 text-xs`}>
{server.isSuspended ? 'Suspended' : 'Connection Error'}
</span>
</div>
: :
server.isInstalling ? server.isInstalling ?
<div css={tw`flex-1 text-center`}> <div css={tw`flex-1 text-center`}>
@ -112,11 +118,7 @@ export default ({ server, className }: { server: Server; className?: string }) =
</span> </span>
</div> </div>
: :
<div css={tw`flex-1 text-center`}> <Spinner size={'small'}/>
<span css={tw`bg-red-500 rounded px-2 py-1 text-red-100 text-xs`}>
{server.isSuspended ? 'Suspended' : 'Connection Error'}
</span>
</div>
: :
<React.Fragment> <React.Fragment>
<div css={tw`flex-1 flex md:ml-4 sm:flex hidden justify-center`}> <div css={tw`flex-1 flex md:ml-4 sm:flex hidden justify-center`}>

View file

@ -7,7 +7,6 @@ import { object, string } from 'yup';
import { Actions, useStoreActions } from 'easy-peasy'; import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state'; import { ApplicationStore } from '@/state';
import disableAccountTwoFactor from '@/api/account/disableAccountTwoFactor'; import disableAccountTwoFactor from '@/api/account/disableAccountTwoFactor';
import { httpErrorToHuman } from '@/api/http';
import tw from 'twin.macro'; import tw from 'twin.macro';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
@ -16,11 +15,10 @@ interface Values {
} }
export default ({ ...props }: RequiredModalProps) => { export default ({ ...props }: RequiredModalProps) => {
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes); const { clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData); const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData);
const submit = ({ password }: Values, { setSubmitting }: FormikHelpers<Values>) => { const submit = ({ password }: Values, { setSubmitting }: FormikHelpers<Values>) => {
clearFlashes('account:two-factor');
disableAccountTwoFactor(password) disableAccountTwoFactor(password)
.then(() => { .then(() => {
updateUserData({ useTotp: false }); updateUserData({ useTotp: false });
@ -29,7 +27,7 @@ export default ({ ...props }: RequiredModalProps) => {
.catch(error => { .catch(error => {
console.error(error); console.error(error);
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' }); clearAndAddHttpError({ error, key: 'account:two-factor' });
setSubmitting(false); setSubmitting(false);
}); });
}; };

View file

@ -6,7 +6,6 @@ import getTwoFactorTokenUrl from '@/api/account/getTwoFactorTokenUrl';
import enableAccountTwoFactor from '@/api/account/enableAccountTwoFactor'; import enableAccountTwoFactor from '@/api/account/enableAccountTwoFactor';
import { Actions, useStoreActions } from 'easy-peasy'; import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state'; import { ApplicationStore } from '@/state';
import { httpErrorToHuman } from '@/api/http';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import Field from '@/components/elements/Field'; import Field from '@/components/elements/Field';
import tw from 'twin.macro'; import tw from 'twin.macro';
@ -22,20 +21,18 @@ export default ({ onDismissed, ...props }: RequiredModalProps) => {
const [ recoveryTokens, setRecoveryTokens ] = useState<string[]>([]); const [ recoveryTokens, setRecoveryTokens ] = useState<string[]>([]);
const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData); const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData);
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes); const { clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
useEffect(() => { useEffect(() => {
clearFlashes('account:two-factor');
getTwoFactorTokenUrl() getTwoFactorTokenUrl()
.then(setToken) .then(setToken)
.catch(error => { .catch(error => {
console.error(error); console.error(error);
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' }); clearAndAddHttpError({ error, key: 'account:two-factor' });
}); });
}, []); }, []);
const submit = ({ code }: Values, { setSubmitting }: FormikHelpers<Values>) => { const submit = ({ code }: Values, { setSubmitting }: FormikHelpers<Values>) => {
clearFlashes('account:two-factor');
enableAccountTwoFactor(code) enableAccountTwoFactor(code)
.then(tokens => { .then(tokens => {
setRecoveryTokens(tokens); setRecoveryTokens(tokens);
@ -43,7 +40,7 @@ export default ({ onDismissed, ...props }: RequiredModalProps) => {
.catch(error => { .catch(error => {
console.error(error); console.error(error);
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' }); clearAndAddHttpError({ error, key: 'account:two-factor' });
}) })
.then(() => setSubmitting(false)); .then(() => setSubmitting(false));
}; };

View file

@ -0,0 +1,63 @@
import React, { useCallback, useEffect, useState } from 'react';
import CopyToClipboard from 'react-copy-to-clipboard';
import tw from 'twin.macro';
import styled, { keyframes } from 'styled-components/macro';
import Fade from '@/components/elements/Fade';
import { SwitchTransition } from 'react-transition-group';
const fade = keyframes`
from { opacity: 0 }
to { opacity: 1 }
`;
const Toast = styled.div`
${tw`fixed z-50 bottom-0 left-0 mb-4 w-full flex justify-end pr-4`};
animation: ${fade} 250ms linear;
& > div {
${tw`rounded px-4 py-2 text-white bg-neutral-800 border border-neutral-900`};
}
`;
const CopyOnClick: React.FC<{ text: string }> = ({ text, children }) => {
const [ copied, setCopied ] = useState(false);
useEffect(() => {
if (!copied) return;
const timeout = setTimeout(() => {
setCopied(false);
}, 2500);
return () => {
clearTimeout(timeout);
};
}, [ copied ]);
const onCopy = useCallback(() => {
setCopied(true);
}, []);
return (
<>
<SwitchTransition>
<Fade timeout={250} key={copied ? 'visible' : 'invisible'}>
{copied ?
<Toast>
<div>
<p>Copied &quot;{text}&quot; to clipboard.</p>
</div>
</Toast>
:
<></>
}
</Fade>
</SwitchTransition>
<CopyToClipboard onCopy={onCopy} text={text} options={{ debug: true }}>
{children}
</CopyToClipboard>
</>
);
};
export default CopyOnClick;

View file

@ -0,0 +1,39 @@
import React from 'react';
import tw from 'twin.macro';
import Icon from '@/components/elements/Icon';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
interface State {
hasError: boolean;
}
// eslint-disable-next-line @typescript-eslint/ban-types
class ErrorBoundary extends React.Component<{}, State> {
state: State = {
hasError: false,
};
static getDerivedStateFromError () {
return { hasError: true };
}
componentDidCatch (error: Error) {
console.error(error);
}
render () {
return this.state.hasError ?
<div css={tw`flex items-center justify-center w-full my-4`}>
<div css={tw`flex items-center bg-neutral-900 rounded p-3 text-red-500`}>
<Icon icon={faExclamationTriangle} css={tw`h-4 w-auto mr-2`}/>
<p css={tw`text-sm text-neutral-100`}>
An error was encountered by the application while rendering this view. Try refreshing the page.
</p>
</div>
</div>
:
this.props.children;
}
}
export default ErrorBoundary;

View file

@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useRef } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { ITerminalOptions, Terminal } from 'xterm'; import { ITerminalOptions, Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit'; import { FitAddon } from 'xterm-addon-fit';
import { SearchAddon } from 'xterm-addon-search'; import { SearchAddon } from 'xterm-addon-search';
@ -11,6 +11,7 @@ import tw, { theme as th } from 'twin.macro';
import 'xterm/css/xterm.css'; import 'xterm/css/xterm.css';
import useEventListener from '@/plugins/useEventListener'; import useEventListener from '@/plugins/useEventListener';
import { debounce } from 'debounce'; import { debounce } from 'debounce';
import { usePersistedState } from '@/plugins/usePersistedState';
const theme = { const theme = {
background: th`colors.black`.toString(), background: th`colors.black`.toString(),
@ -63,6 +64,9 @@ export default () => {
const searchBar = new SearchBarAddon({ searchAddon }); const searchBar = new SearchBarAddon({ searchAddon });
const { connected, instance } = ServerContext.useStoreState(state => state.socket); const { connected, instance } = ServerContext.useStoreState(state => state.socket);
const [ canSendCommands ] = usePermissions([ 'control.console' ]); const [ canSendCommands ] = usePermissions([ 'control.console' ]);
const serverId = ServerContext.useStoreState(state => state.server.data!.id);
const [ history, setHistory ] = usePersistedState<string[]>(`${serverId}:command_history`, []);
const [ historyIndex, setHistoryIndex ] = useState(-1);
const handleConsoleOutput = (line: string, prelude = false) => terminal.writeln( const handleConsoleOutput = (line: string, prelude = false) => terminal.writeln(
(prelude ? TERMINAL_PRELUDE : '') + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m', (prelude ? TERMINAL_PRELUDE : '') + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m',
@ -77,12 +81,28 @@ export default () => {
); );
const handleCommandKeydown = (e: React.KeyboardEvent<HTMLInputElement>) => { const handleCommandKeydown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key !== 'Enter' || (e.key === 'Enter' && e.currentTarget.value.length < 1)) { if (e.key === 'ArrowUp') {
return; const newIndex = Math.min(historyIndex + 1, history!.length - 1);
setHistoryIndex(newIndex);
e.currentTarget.value = history![newIndex] || '';
} }
instance && instance.send('send command', e.currentTarget.value); if (e.key === 'ArrowDown') {
const newIndex = Math.max(historyIndex - 1, -1);
setHistoryIndex(newIndex);
e.currentTarget.value = history![newIndex] || '';
}
const command = e.currentTarget.value;
if (e.key === 'Enter' && command.length > 0) {
setHistory(prevHistory => [ command, ...prevHistory! ].slice(0, 32));
setHistoryIndex(-1);
instance && instance.send('send command', command);
e.currentTarget.value = ''; e.currentTarget.value = '';
}
}; };
useEffect(() => { useEffect(() => {

View file

@ -16,6 +16,7 @@ import Select from '@/components/elements/Select';
import modes from '@/modes'; import modes from '@/modes';
import useFlash from '@/plugins/useFlash'; import useFlash from '@/plugins/useFlash';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import ErrorBoundary from '@/components/elements/ErrorBoundary';
const LazyCodemirrorEditor = lazy(() => import(/* webpackChunkName: "editor" */'@/components/elements/CodemirrorEditor')); const LazyCodemirrorEditor = lazy(() => import(/* webpackChunkName: "editor" */'@/components/elements/CodemirrorEditor'));
@ -60,9 +61,7 @@ export default () => {
setLoading(true); setLoading(true);
clearFlashes('files:view'); clearFlashes('files:view');
fetchFileContent() fetchFileContent()
.then(content => { .then(content => saveFileContents(uuid, encodeURIComponent(name || hash.replace(/^#/, '')), content))
return saveFileContents(uuid, name || hash.replace(/^#/, ''), content);
})
.then(() => { .then(() => {
if (name) { if (name) {
history.push(`/server/${id}/files/edit#/${name}`); history.push(`/server/${id}/files/edit#/${name}`);
@ -87,7 +86,9 @@ export default () => {
return ( return (
<PageContentBlock> <PageContentBlock>
<FlashMessageRender byKey={'files:view'} css={tw`mb-4`}/> <FlashMessageRender byKey={'files:view'} css={tw`mb-4`}/>
<ErrorBoundary>
<FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'}/> <FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'}/>
</ErrorBoundary>
{hash.replace(/^#/, '').endsWith('.pteroignore') && {hash.replace(/^#/, '').endsWith('.pteroignore') &&
<div css={tw`mb-4 p-4 border-l-4 bg-neutral-900 rounded border-cyan-400`}> <div css={tw`mb-4 p-4 border-l-4 bg-neutral-900 rounded border-cyan-400`}>
<p css={tw`text-neutral-300 text-sm`}> <p css={tw`text-neutral-300 text-sm`}>

View file

@ -33,10 +33,10 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
.filter(directory => !!directory) .filter(directory => !!directory)
.map((directory, index, dirs) => { .map((directory, index, dirs) => {
if (!withinFileEditor && index === dirs.length - 1) { if (!withinFileEditor && index === dirs.length - 1) {
return { name: decodeURIComponent(directory) }; return { name: decodeURIComponent(encodeURIComponent(directory)) };
} }
return { name: decodeURIComponent(directory), path: `/${dirs.slice(0, index + 1).join('/')}` }; return { name: decodeURIComponent(encodeURIComponent(directory)), path: `/${dirs.slice(0, index + 1).join('/')}` };
}); });
const onSelectAllClick = (e: React.ChangeEvent<HTMLInputElement>) => { const onSelectAllClick = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -79,7 +79,7 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
} }
{file && {file &&
<React.Fragment> <React.Fragment>
<span css={tw`px-1 text-neutral-300`}>{decodeURIComponent(file)}</span> <span css={tw`px-1 text-neutral-300`}>{decodeURIComponent(encodeURIComponent(file))}</span>
</React.Fragment> </React.Fragment>
} }
</div> </div>

View file

@ -17,6 +17,7 @@ import MassActionsBar from '@/components/server/files/MassActionsBar';
import UploadButton from '@/components/server/files/UploadButton'; import UploadButton from '@/components/server/files/UploadButton';
import ServerContentBlock from '@/components/elements/ServerContentBlock'; import ServerContentBlock from '@/components/elements/ServerContentBlock';
import { useStoreActions } from '@/state/hooks'; import { useStoreActions } from '@/state/hooks';
import ErrorBoundary from '@/components/elements/ErrorBoundary';
const sortFiles = (files: FileObject[]): FileObject[] => { const sortFiles = (files: FileObject[]): FileObject[] => {
return files.sort((a, b) => a.name.localeCompare(b.name)) return files.sort((a, b) => a.name.localeCompare(b.name))
@ -50,7 +51,9 @@ export default () => {
return ( return (
<ServerContentBlock title={'File Manager'} showFlashKey={'files'}> <ServerContentBlock title={'File Manager'} showFlashKey={'files'}>
<ErrorBoundary>
<FileManagerBreadcrumbs/> <FileManagerBreadcrumbs/>
</ErrorBoundary>
{ {
!files ? !files ?
<Spinner size={'large'} centered/> <Spinner size={'large'} centered/>
@ -81,6 +84,7 @@ export default () => {
</CSSTransition> </CSSTransition>
} }
<Can action={'file.create'}> <Can action={'file.create'}>
<ErrorBoundary>
<div css={tw`flex flex-wrap-reverse justify-end mt-4`}> <div css={tw`flex flex-wrap-reverse justify-end mt-4`}>
<NewDirectoryButton css={tw`w-full flex-none mt-4 sm:mt-0 sm:w-auto sm:mr-4`}/> <NewDirectoryButton css={tw`w-full flex-none mt-4 sm:mt-0 sm:w-auto sm:mr-4`}/>
<UploadButton css={tw`flex-1 mr-4 sm:flex-none sm:mt-0`}/> <UploadButton css={tw`flex-1 mr-4 sm:flex-none sm:mt-0`}/>
@ -93,6 +97,7 @@ export default () => {
</Button> </Button>
</NavLink> </NavLink>
</div> </div>
</ErrorBoundary>
</Can> </Can>
</> </>
} }

View file

@ -92,9 +92,9 @@ export default ({ className }: WithClassname) => {
<span css={tw`text-neutral-200`}>This directory will be created as</span> <span css={tw`text-neutral-200`}>This directory will be created as</span>
&nbsp;/home/container/ &nbsp;/home/container/
<span css={tw`text-cyan-200`}> <span css={tw`text-cyan-200`}>
{decodeURIComponent( {decodeURIComponent(encodeURIComponent(
join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, ''), join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, ''),
)} ))}
</span> </span>
</p> </p>
<div css={tw`flex justify-end`}> <div css={tw`flex justify-end`}>

View file

@ -14,6 +14,7 @@ import { debounce } from 'debounce';
import setServerAllocationNotes from '@/api/server/network/setServerAllocationNotes'; import setServerAllocationNotes from '@/api/server/network/setServerAllocationNotes';
import useFlash from '@/plugins/useFlash'; import useFlash from '@/plugins/useFlash';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import CopyOnClick from '@/components/elements/CopyOnClick';
const Code = styled.code`${tw`font-mono py-1 px-2 bg-neutral-900 rounded text-sm inline-block`}`; 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`}`; const Label = styled.label`${tw`uppercase text-xs mt-1 text-neutral-400 block px-1 select-none transition-colors duration-150`}`;
@ -43,11 +44,12 @@ const AllocationRow = ({ allocation, onSetPrimary, onNotesChanged }: Props) => {
<GreyRowBox $hoverable={false} css={tw`flex-wrap md:flex-no-wrap mt-2`}> <GreyRowBox $hoverable={false} css={tw`flex-wrap md:flex-no-wrap mt-2`}>
<div css={tw`flex items-center w-full md:w-auto`}> <div css={tw`flex items-center w-full md:w-auto`}>
<div css={tw`pl-4 pr-6 text-neutral-400`}> <div css={tw`pl-4 pr-6 text-neutral-400`}>
<FontAwesomeIcon icon={faNetworkWired}/> <FontAwesomeIcon icon={faNetworkWired} />
</div> </div>
<div css={tw`mr-4 flex-1 md:w-40`}> <div css={tw`mr-4 flex-1 md:w-40`}>
<Code>{allocation.alias || allocation.ip}</Code> {allocation.alias ? <CopyOnClick text={allocation.alias}><Code css={tw`w-40 truncate`}>{allocation.alias}</Code></CopyOnClick> :
<Label>IP Address</Label> <CopyOnClick text={allocation.ip}><Code>{allocation.ip}</Code></CopyOnClick>}
<Label>{allocation.alias ? 'Hostname' : 'IP Address'}</Label>
</div> </div>
<div css={tw`w-16 md:w-24 overflow-hidden`}> <div css={tw`w-16 md:w-24 overflow-hidden`}>
<Code>{allocation.port}</Code> <Code>{allocation.port}</Code>

View file

@ -10,7 +10,7 @@ export default () => {
return ( return (
<> <>
{visible && <EditSubuserModal appear visible onDismissed={() => setVisible(false)}/>} <EditSubuserModal visible={visible} onModalDismissed={() => setVisible(false)}/>
<Button onClick={() => setVisible(true)}> <Button onClick={() => setVisible(true)}>
<FontAwesomeIcon icon={faUserPlus} css={tw`mr-1`}/> New User <FontAwesomeIcon icon={faUserPlus} css={tw`mr-1`}/> New User
</Button> </Button>

View file

@ -0,0 +1,65 @@
import styled from 'styled-components/macro';
import tw from 'twin.macro';
import Checkbox from '@/components/elements/Checkbox';
import React from 'react';
import { useStoreState } from 'easy-peasy';
import Label from '@/components/elements/Label';
const Container = styled.label`
${tw`flex items-center border border-transparent rounded md:p-2 transition-colors duration-75`};
text-transform: none;
&:not(.disabled) {
${tw`cursor-pointer`};
&:hover {
${tw`border-neutral-500 bg-neutral-800`};
}
}
&:not(:first-of-type) {
${tw`mt-4 sm:mt-2`};
}
&.disabled {
${tw`opacity-50`};
& input[type="checkbox"]:not(:checked) {
${tw`border-0`};
}
}
`;
interface Props {
permission: string;
disabled: boolean;
}
const PermissionRow = ({ permission, disabled }: Props) => {
const [ key, pkey ] = permission.split('.', 2);
const permissions = useStoreState(state => state.permissions.data);
return (
<Container htmlFor={`permission_${permission}`} className={disabled ? 'disabled' : undefined}>
<div css={tw`p-2`}>
<Checkbox
id={`permission_${permission}`}
name={'permissions'}
value={permission}
css={tw`w-5 h-5 mr-2`}
disabled={disabled}
/>
</div>
<div css={tw`flex-1`}>
<Label as={'p'} css={tw`font-medium`}>{pkey}</Label>
{permissions[key].keys[pkey].length > 0 &&
<p css={tw`text-xs text-neutral-400 mt-1`}>
{permissions[key].keys[pkey]}
</p>
}
</div>
</Container>
);
};
export default PermissionRow;

View file

@ -0,0 +1,50 @@
import React, { memo, useCallback } from 'react';
import { useField } from 'formik';
import TitledGreyBox from '@/components/elements/TitledGreyBox';
import tw from 'twin.macro';
import Input from '@/components/elements/Input';
import isEqual from 'react-fast-compare';
interface Props {
isEditable: boolean;
title: string;
permissions: string[];
className?: string;
}
const PermissionTitleBox: React.FC<Props> = memo(({ isEditable, title, permissions, className, children }) => {
const [ { value }, , { setValue } ] = useField<string[]>('permissions');
const onCheckboxClicked = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
if (e.currentTarget.checked) {
setValue([
...value,
...permissions.filter(p => !value.includes(p)),
]);
} else {
setValue(value.filter(p => !permissions.includes(p)));
}
}, [ permissions, value ]);
return (
<TitledGreyBox
title={
<div css={tw`flex items-center`}>
<p css={tw`text-sm uppercase flex-1`}>{title}</p>
{isEditable &&
<Input
type={'checkbox'}
checked={permissions.every(p => value.includes(p))}
onChange={onCheckboxClicked}
/>
}
</div>
}
className={className}
>
{children}
</TitledGreyBox>
);
}, isEqual);
export default PermissionTitleBox;

View file

@ -19,14 +19,11 @@ export default ({ subuser }: Props) => {
return ( return (
<GreyRowBox css={tw`mb-2`}> <GreyRowBox css={tw`mb-2`}>
{visible &&
<EditSubuserModal <EditSubuserModal
appear
visible
subuser={subuser} subuser={subuser}
onDismissed={() => setVisible(false)} visible={visible}
onModalDismissed={() => setVisible(false)}
/> />
}
<div css={tw`w-10 h-10 rounded-full bg-white border-2 border-neutral-800 overflow-hidden hidden md:block`}> <div css={tw`w-10 h-10 rounded-full bg-white border-2 border-neutral-800 overflow-hidden hidden md:block`}>
<img css={tw`w-full h-full`} src={`${subuser.image}?s=400`}/> <img css={tw`w-full h-full`} src={`${subuser.image}?s=400`}/>
</div> </div>

View file

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import Modal, { ModalProps } from '@/components/elements/Modal'; import Modal, { ModalProps } from '@/components/elements/Modal';
import ModalContext from '@/context/ModalContext'; import ModalContext from '@/context/ModalContext';
import isEqual from 'react-fast-compare';
export interface AsModalProps { export interface AsModalProps {
visible: boolean; visible: boolean;
@ -13,7 +12,7 @@ type ExtendedModalProps = Omit<ModalProps, 'appear' | 'visible' | 'onDismissed'>
interface State { interface State {
render: boolean; render: boolean;
visible: boolean; visible: boolean;
modalProps: ExtendedModalProps | undefined; showSpinnerOverlay?: boolean;
} }
type ExtendedComponentType<T> = (C: React.ComponentType<T>) => React.ComponentType<T & AsModalProps>; type ExtendedComponentType<T> = (C: React.ComponentType<T>) => React.ComponentType<T & AsModalProps>;
@ -30,17 +29,18 @@ function asModal<P extends object> (modalProps?: ExtendedModalProps | ((props: P
this.state = { this.state = {
render: props.visible, render: props.visible,
visible: props.visible, visible: props.visible,
modalProps: typeof modalProps === 'function' ? modalProps(this.props) : modalProps, showSpinnerOverlay: undefined,
};
}
get modalProps () {
return {
...(typeof modalProps === 'function' ? modalProps(this.props) : modalProps),
showSpinnerOverlay: this.state.showSpinnerOverlay,
}; };
} }
componentDidUpdate (prevProps: Readonly<P & AsModalProps>) { componentDidUpdate (prevProps: Readonly<P & AsModalProps>) {
const mapped = typeof modalProps === 'function' ? modalProps(this.props) : modalProps;
if (!isEqual(this.state.modalProps, mapped)) {
// noinspection JSPotentiallyInvalidUsageOfThis
this.setState({ modalProps: mapped });
}
if (prevProps.visible && !this.props.visible) { if (prevProps.visible && !this.props.visible) {
// noinspection JSPotentiallyInvalidUsageOfThis // noinspection JSPotentiallyInvalidUsageOfThis
this.setState({ visible: false }); this.setState({ visible: false });
@ -52,22 +52,10 @@ function asModal<P extends object> (modalProps?: ExtendedModalProps | ((props: P
dismiss = () => this.setState({ visible: false }); dismiss = () => this.setState({ visible: false });
toggleSpinner = (value?: boolean) => this.setState(s => ({ toggleSpinner = (value?: boolean) => this.setState({ showSpinnerOverlay: value });
modalProps: {
...s.modalProps,
showSpinnerOverlay: value || false,
},
}));
render () { render () {
return ( return (
<ModalContext.Provider
value={{
dismiss: this.dismiss.bind(this),
toggleSpinner: this.toggleSpinner.bind(this),
}}
>
{
this.state.render ? this.state.render ?
<Modal <Modal
appear appear
@ -77,14 +65,19 @@ function asModal<P extends object> (modalProps?: ExtendedModalProps | ((props: P
this.props.onModalDismissed(); this.props.onModalDismissed();
} }
})} })}
{...this.state.modalProps} {...this.modalProps}
>
<ModalContext.Provider
value={{
dismiss: this.dismiss.bind(this),
toggleSpinner: this.toggleSpinner.bind(this),
}}
> >
<Component {...this.props}/> <Component {...this.props}/>
</ModalContext.Provider>
</Modal> </Modal>
: :
null null
}
</ModalContext.Provider>
); );
} }
}; };

View file

@ -28,6 +28,7 @@ import NetworkContainer from '@/components/server/network/NetworkContainer';
import InstallListener from '@/components/server/InstallListener'; import InstallListener from '@/components/server/InstallListener';
import StartupContainer from '@/components/server/startup/StartupContainer'; import StartupContainer from '@/components/server/startup/StartupContainer';
import requireServerPermission from '@/hoc/requireServerPermission'; import requireServerPermission from '@/hoc/requireServerPermission';
import ErrorBoundary from '@/components/elements/ErrorBoundary';
const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => { const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => {
const rootAdmin = useStoreState(state => state.user.data!.rootAdmin); const rootAdmin = useStoreState(state => state.user.data!.rootAdmin);
@ -120,7 +121,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
message={'Please check back in a few minutes.'} message={'Please check back in a few minutes.'}
/> />
: :
<> <ErrorBoundary>
<TransitionRouter> <TransitionRouter>
<Switch location={location}> <Switch location={location}>
<Route path={`${match.path}`} component={ServerConsole} exact/> <Route path={`${match.path}`} component={ServerConsole} exact/>
@ -173,7 +174,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
<Route path={'*'} component={NotFound}/> <Route path={'*'} component={NotFound}/>
</Switch> </Switch>
</TransitionRouter> </TransitionRouter>
</> </ErrorBoundary>
} }
</> </>
} }

View file

@ -11,7 +11,7 @@
@endsection @endsection
@section('content-header') @section('content-header')
<h1>Mounts<small>SoonTM</small></h1> <h1>Mounts<small>Configure and manage additional mount points for servers.</small></h1>
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li> <li><a href="{{ route('admin.index') }}">Admin</a></li>
<li class="active">Mounts</li> <li class="active">Mounts</li>

View file

@ -26,7 +26,7 @@
<div class="box-tools search01"> <div class="box-tools search01">
<form action="{{ route('admin.servers') }}" method="GET"> <form action="{{ route('admin.servers') }}" method="GET">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input type="text" name="filter[name]" class="form-control pull-right" value="{{ request()->input('filter.name') }}" placeholder="Search Servers"> <input type="text" name="filter[*]" class="form-control pull-right" value="{{ request()->input('filter[*]') }}" placeholder="Search Servers">
<div class="input-group-btn"> <div class="input-group-btn">
<button type="submit" class="btn btn-default"><i class="fa fa-search"></i></button> <button type="submit" class="btn btn-default"><i class="fa fa-search"></i></button>
<a href="{{ route('admin.servers.new') }}"><button type="button" class="btn btn-sm btn-primary" style="border-radius: 0 3px 3px 0;margin-left:-1px;">Create New</button></a> <a href="{{ route('admin.servers.new') }}"><button type="button" class="btn btn-sm btn-primary" style="border-radius: 0 3px 3px 0;margin-left:-1px;">Create New</button></a>

View file

@ -96,7 +96,6 @@
</div> </div>
@endif @endif
@if($canTransfer)
<div class="col-sm-4"> <div class="col-sm-4">
<div class="box box-success"> <div class="box box-success">
<div class="box-header with-border"> <div class="box-header with-border">
@ -104,22 +103,26 @@
</div> </div>
<div class="box-body"> <div class="box-body">
<p> <p>
Hopefully, you will soon be able to move servers around without needing to do a bunch of confusing Transfer this server to another node connected to this panel.
operations manually and it will work fluidly and with no problems. <strong>Warning!</strong> This feature has not been fully tested and may have bugs.
</p> </p>
</div> </div>
<div class="box-footer"> <div class="box-footer">
@if($canTransfer)
<button class="btn btn-success" data-toggle="modal" data-target="#transferServerModal">Transfer Server</button> <button class="btn btn-success" data-toggle="modal" data-target="#transferServerModal">Transfer Server</button>
</div> @else
</div> <button class="btn btn-success disabled">Transfer Server</button>
</div> <p style="padding-top: 1rem;">Transferring a server requires more than one node to be configured on your panel.</p>
@endif @endif
</div> </div>
</div>
</div>
</div>
<div class="modal fade" id="transferServerModal" tabindex="-1" role="dialog"> <div class="modal fade" id="transferServerModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<!-- TODO: Change route -->
<form action="{{ route('admin.servers.view.manage.transfer', $server->id) }}" method="POST"> <form action="{{ route('admin.servers.view.manage.transfer', $server->id) }}" method="POST">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>

View file

@ -64,7 +64,7 @@
@endif @endif
</td> </td>
<td class="text-center"> <td class="text-center">
<a href="{{ route('admin.servers', ['query' => $user->email]) }}">{{ $user->servers_count }}</a> <a href="{{ route('admin.servers', ['filter[owner_id]' => $user->id]) }}">{{ $user->servers_count }}</a>
</td> </td>
<td class="text-center">{{ $user->subuser_of_count }}</td> <td class="text-center">{{ $user->subuser_of_count }}</td>
<td class="text-center"><img src="https://www.gravatar.com/avatar/{{ md5(strtolower($user->email)) }}?s=100" style="height:20px;" class="img-circle" /></td> <td class="text-center"><img src="https://www.gravatar.com/avatar/{{ md5(strtolower($user->email)) }}?s=100" style="height:20px;" class="img-circle" /></td>

View file

@ -44,7 +44,8 @@ class ExecuteScheduleTest extends ClientApiIntegrationTestCase
$this->actingAs($user)->postJson($this->link($schedule, '/execute'))->assertStatus(Response::HTTP_ACCEPTED); $this->actingAs($user)->postJson($this->link($schedule, '/execute'))->assertStatus(Response::HTTP_ACCEPTED);
Bus::assertDispatched(function (RunTaskJob $job) use ($task) { Bus::assertDispatched(function (RunTaskJob $job) use ($task) {
$this->assertSame($task->time_offset, $job->delay); // A task executed right now should not have any job delay associated with it.
$this->assertNull($job->delay);
$this->assertSame($task->id, $job->task->id); $this->assertSame($task->id, $job->task->id);
return true; return true;

View file

@ -0,0 +1,84 @@
<?php
namespace Pterodactyl\Tests\Integration\Api\Client\Server\ScheduleTask;
use Pterodactyl\Models\Task;
use Pterodactyl\Models\User;
use Illuminate\Http\Response;
use Pterodactyl\Models\Schedule;
use Pterodactyl\Models\Permission;
use Pterodactyl\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
class DeleteScheduleTaskTest extends ClientApiIntegrationTestCase
{
/**
* Test that an error is returned if the schedule does not belong to the server.
*/
public function testScheduleNotBelongingToServerReturnsError()
{
$server2 = $this->createServerModel();
[$user] = $this->generateTestAccount();
$schedule = factory(Schedule::class)->create(['server_id' => $server2->id]);
$task = factory(Task::class)->create(['schedule_id' => $schedule->id]);
$this->actingAs($user)->deleteJson($this->link($task))->assertNotFound();
}
/**
* Test that an error is returned if the task and schedule in the URL do not line up
* with eachother.
*/
public function testTaskBelongingToDifferentScheduleReturnsError()
{
[$user, $server] = $this->generateTestAccount();
$schedule = factory(Schedule::class)->create(['server_id' => $server->id]);
$schedule2 = factory(Schedule::class)->create(['server_id' => $server->id]);
$task = factory(Task::class)->create(['schedule_id' => $schedule->id]);
$this->actingAs($user)->deleteJson("/api/client/servers/{$server->uuid}/schedules/{$schedule2->id}/tasks/{$task->id}")->assertNotFound();
}
/**
* Test that a user without the required permissions returns an error.
*/
public function testUserWithoutPermissionReturnsError()
{
[$user, $server] = $this->generateTestAccount([Permission::ACTION_SCHEDULE_CREATE]);
$schedule = factory(Schedule::class)->create(['server_id' => $server->id]);
$task = factory(Task::class)->create(['schedule_id' => $schedule->id]);
$this->actingAs($user)->deleteJson($this->link($task))->assertForbidden();
$user2 = factory(User::class)->create();
$this->actingAs($user2)->deleteJson($this->link($task))->assertNotFound();
}
/**
* Test that a schedule task is deleted and items with a higher sequence ID are decremented
* properly in the database.
*/
public function testScheduleTaskIsDeletedAndSubsequentTasksAreUpdated()
{
[$user, $server] = $this->generateTestAccount();
$schedule = factory(Schedule::class)->create(['server_id' => $server->id]);
$tasks = [
factory(Task::class)->create(['schedule_id' => $schedule->id, 'sequence_id' => 1]),
factory(Task::class)->create(['schedule_id' => $schedule->id, 'sequence_id' => 2]),
factory(Task::class)->create(['schedule_id' => $schedule->id, 'sequence_id' => 3]),
factory(Task::class)->create(['schedule_id' => $schedule->id, 'sequence_id' => 4]),
];
$response = $this->actingAs($user)->deleteJson($this->link($tasks[1]));
$response->assertStatus(Response::HTTP_NO_CONTENT);
$this->assertDatabaseHas('tasks', ['id' => $tasks[0]->id, 'sequence_id' => 1]);
$this->assertDatabaseHas('tasks', ['id' => $tasks[2]->id, 'sequence_id' => 2]);
$this->assertDatabaseHas('tasks', ['id' => $tasks[3]->id, 'sequence_id' => 3]);
$this->assertDatabaseMissing('tasks', ['id' => $tasks[1]->id]);
}
}

View file

@ -3,6 +3,8 @@
namespace Pterodactyl\Tests\Integration\Services\Schedules; namespace Pterodactyl\Tests\Integration\Services\Schedules;
use Mockery; use Mockery;
use Exception;
use Carbon\CarbonImmutable;
use Pterodactyl\Models\Task; use Pterodactyl\Models\Task;
use InvalidArgumentException; use InvalidArgumentException;
use Pterodactyl\Models\Schedule; use Pterodactyl\Models\Schedule;
@ -61,7 +63,7 @@ class ProcessScheduleServiceTest extends IntegrationTestCase
*/ */
public function testJobCanBeDispatchedWithExpectedInitialDelay($now) public function testJobCanBeDispatchedWithExpectedInitialDelay($now)
{ {
$this->swap(Dispatcher::class, $dispatcher = Mockery::mock(Dispatcher::class)); Bus::fake();
$server = $this->createServerModel(); $server = $this->createServerModel();
@ -71,16 +73,84 @@ class ProcessScheduleServiceTest extends IntegrationTestCase
/** @var \Pterodactyl\Models\Task $task */ /** @var \Pterodactyl\Models\Task $task */
$task = factory(Task::class)->create(['schedule_id' => $schedule->id, 'time_offset' => 10, 'sequence_id' => 1]); $task = factory(Task::class)->create(['schedule_id' => $schedule->id, 'time_offset' => 10, 'sequence_id' => 1]);
$dispatcher->expects($now ? 'dispatchNow' : 'dispatch')->with(Mockery::on(function (RunTaskJob $job) use ($task) {
return $task->id === $job->task->id && $job->delay === 10;
}));
$this->getService()->handle($schedule, $now); $this->getService()->handle($schedule, $now);
Bus::assertDispatched(RunTaskJob::class, function ($job) use ($now, $task) {
$this->assertInstanceOf(RunTaskJob::class, $job);
$this->assertSame($task->id, $job->task->id);
// Jobs using dispatchNow should not have a delay associated with them.
$this->assertSame($now ? null : 10, $job->delay);
return true;
});
$this->assertDatabaseHas('schedules', ['id' => $schedule->id, 'is_processing' => true]); $this->assertDatabaseHas('schedules', ['id' => $schedule->id, 'is_processing' => true]);
$this->assertDatabaseHas('tasks', ['id' => $task->id, 'is_queued' => true]); $this->assertDatabaseHas('tasks', ['id' => $task->id, 'is_queued' => true]);
} }
/**
* Test that even if a schedule's task sequence gets messed up the first task based on
* the ascending order of tasks is used.
*
* @see https://github.com/pterodactyl/panel/issues/2534
*/
public function testFirstSequenceTaskIsFound()
{
Bus::fake();
$server = $this->createServerModel();
/** @var \Pterodactyl\Models\Schedule $schedule */
$schedule = factory(Schedule::class)->create(['server_id' => $server->id]);
/** @var \Pterodactyl\Models\Task $task */
$task2 = factory(Task::class)->create(['schedule_id' => $schedule->id, 'sequence_id' => 4]);
$task = factory(Task::class)->create(['schedule_id' => $schedule->id, 'sequence_id' => 2]);
$task3 = factory(Task::class)->create(['schedule_id' => $schedule->id, 'sequence_id' => 3]);
$this->getService()->handle($schedule);
Bus::assertDispatched(RunTaskJob::class, function (RunTaskJob $job) use ($task) {
return $task->id === $job->task->id;
});
$this->assertDatabaseHas('schedules', ['id' => $schedule->id, 'is_processing' => true]);
$this->assertDatabaseHas('tasks', ['id' => $task->id, 'is_queued' => true]);
$this->assertDatabaseHas('tasks', ['id' => $task2->id, 'is_queued' => false]);
$this->assertDatabaseHas('tasks', ['id' => $task3->id, 'is_queued' => false]);
}
/**
* Tests that a task's processing state is reset correctly if using "dispatchNow" and there is
* an exception encountered while running it.
*
* @see https://github.com/pterodactyl/panel/issues/2550
*/
public function testTaskDispatchedNowIsResetProperlyIfErrorIsEncountered()
{
$this->swap(Dispatcher::class, $dispatcher = Mockery::mock(Dispatcher::class));
$server = $this->createServerModel();
/** @var \Pterodactyl\Models\Schedule $schedule */
$schedule = factory(Schedule::class)->create(['server_id' => $server->id, 'last_run_at' => null]);
/** @var \Pterodactyl\Models\Task $task */
$task = factory(Task::class)->create(['schedule_id' => $schedule->id, 'sequence_id' => 1]);
$dispatcher->expects('dispatchNow')->andThrows(new Exception('Test thrown exception'));
$this->expectException(Exception::class);
$this->expectExceptionMessage('Test thrown exception');
$this->getService()->handle($schedule, true);
$this->assertDatabaseHas('schedules', [
'id' => $schedule->id,
'is_processing' => false,
'last_run_at' => CarbonImmutable::now()->toAtomString(),
]);
$this->assertDatabaseHas('tasks', ['id' => $task->id, 'is_queued' => false]);
}
/** /**
* @return array * @return array
*/ */

View file

@ -0,0 +1,242 @@
<?php
namespace Pterodactyl\Tests\Integration\Services\Servers;
use Mockery;
use Exception;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Allocation;
use Pterodactyl\Exceptions\DisplayException;
use GuzzleHttp\Exception\BadResponseException;
use Pterodactyl\Tests\Integration\IntegrationTestCase;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Services\Servers\BuildModificationService;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
class BuildModificationServiceTest extends IntegrationTestCase
{
/** @var \Mockery\MockInterface */
private $daemonServerRepository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->daemonServerRepository = Mockery::mock(DaemonServerRepository::class);
$this->swap(DaemonServerRepository::class, $this->daemonServerRepository);
}
/**
* Test that allocations can be added and removed from a server. Only the allocations on the
* current node and belonging to this server should be modified.
*/
public function testAllocationsCanBeModifiedForTheServer()
{
$server = $this->createServerModel();
$server2 = $this->createServerModel();
/** @var \Pterodactyl\Models\Allocation[] $allocations */
$allocations = factory(Allocation::class)->times(4)->create(['node_id' => $server->node_id, 'notes' => 'Random notes']);
$initialAllocationId = $server->allocation_id;
$allocations[0]->update(['server_id' => $server->id, 'notes' => 'Test notes']);
// Some additional test allocations for the other server, not the server we are attempting
// to modify.
$allocations[2]->update(['server_id' => $server2->id]);
$allocations[3]->update(['server_id' => $server2->id]);
$this->daemonServerRepository->expects('setServer->update')->andReturnUndefined();
$response = $this->getService()->handle($server, [
// Attempt to add one new allocation, and an allocation assigned to another server. The
// other server allocation should be ignored, and only the allocation for this server should
// be used.
'add_allocations' => [$allocations[2]->id, $allocations[1]->id],
// Remove the default server allocation, ensuring that the new allocation passed through
// in the data becomes the default allocation.
'remove_allocations' => [$server->allocation_id, $allocations[0]->id, $allocations[3]->id],
]);
$this->assertInstanceOf(Server::class, $response);
// Only one allocation should exist for this server now.
$this->assertCount(1, $response->allocations);
$this->assertSame($allocations[1]->id, $response->allocation_id);
$this->assertNull($response->allocation->notes);
// These two allocations should not have been touched.
$this->assertDatabaseHas('allocations', ['id' => $allocations[2]->id, 'server_id' => $server2->id]);
$this->assertDatabaseHas('allocations', ['id' => $allocations[3]->id, 'server_id' => $server2->id]);
// Both of these allocations should have been removed from the server, and have had their
// notes properly reset.
$this->assertDatabaseHas('allocations', ['id' => $initialAllocationId, 'server_id' => null, 'notes' => null]);
$this->assertDatabaseHas('allocations', ['id' => $allocations[0]->id, 'server_id' => null, 'notes' => null]);
}
/**
* Test that an exception is thrown if removing the default allocation without also assigning
* new allocations to the server.
*/
public function testExceptionIsThrownIfRemovingTheDefaultAllocation()
{
$server = $this->createServerModel();
/** @var \Pterodactyl\Models\Allocation[] $allocations */
$allocations = factory(Allocation::class)->times(4)->create(['node_id' => $server->node_id]);
$allocations[0]->update(['server_id' => $server->id]);
$this->expectException(DisplayException::class);
$this->expectExceptionMessage('You are attempting to delete the default allocation for this server but there is no fallback allocation to use.');
$this->getService()->handle($server, [
'add_allocations' => [],
'remove_allocations' => [$server->allocation_id, $allocations[0]->id],
]);
}
/**
* Test that the build data for the server is properly passed along to the Wings instance so that
* the server data is updated in realtime. This test also ensures that only certain fields get updated
* for the server, and not just any arbitrary field.
*/
public function testServerBuildDataIsProperlyUpdatedOnWings()
{
$server = $this->createServerModel();
$this->daemonServerRepository->expects('setServer')->with(Mockery::on(function (Server $s) use ($server) {
return $s->id === $server->id;
}))->andReturnSelf();
$this->daemonServerRepository->expects('update')->with(Mockery::on(function ($data) {
$this->assertEquals([
'memory_limit' => 256,
'swap' => 128,
'io_weight' => 600,
'cpu_limit' => 150,
'threads' => '1,2',
'disk_space' => 1024,
], $data);
return true;
}))->andReturnUndefined();
$response = $this->getService()->handle($server, [
'oom_disabled' => false,
'memory' => 256,
'swap' => 128,
'io' => 600,
'cpu' => 150,
'threads' => '1,2',
'disk' => 1024,
'backup_limit' => null,
'database_limit' => 10,
'allocation_limit' => 20,
]);
$this->assertFalse($response->oom_disabled);
$this->assertSame(256, $response->memory);
$this->assertSame(128, $response->swap);
$this->assertSame(600, $response->io);
$this->assertSame(150, $response->cpu);
$this->assertSame('1,2', $response->threads);
$this->assertSame(1024, $response->disk);
$this->assertSame(0, $response->backup_limit);
$this->assertSame(10, $response->database_limit);
$this->assertSame(20, $response->allocation_limit);
}
/**
* Test that no exception is thrown if we are only removing an allocation.
*/
public function testNoExceptionIsThrownIfOnlyRemovingAllocation()
{
$server = $this->createServerModel();
/** @var \Pterodactyl\Models\Allocation[] $allocations */
$allocation = factory(Allocation::class)->create(['node_id' => $server->node_id, 'server_id' => $server->id]);
$this->daemonServerRepository->expects('setServer->update')->andReturnUndefined();
$this->getService()->handle($server, [
'remove_allocations' => [$allocation->id],
]);
$this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => null]);
}
/**
* Test that allocations in both the add and remove arrays are only added, and not removed.
* This scenario wouldn't really happen in the UI, but it is possible to perform via the API
* so we want to make sure that the logic being used doesn't break if the allocation exists
* in both arrays.
*
* We'll default to adding the allocation in this case.
*/
public function testAllocationInBothAddAndRemoveIsAdded()
{
$server = $this->createServerModel();
/** @var \Pterodactyl\Models\Allocation[] $allocations */
$allocation = factory(Allocation::class)->create(['node_id' => $server->node_id]);
$this->daemonServerRepository->expects('setServer->update')->andReturnUndefined();
$this->getService()->handle($server, [
'add_allocations' => [$allocation->id],
'remove_allocations' => [$allocation->id],
]);
$this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => $server->id]);
}
/**
* Test that using the same allocation ID multiple times in the array does not cause an error.
*/
public function testUsingSameAllocationIdMultipleTimesDoesNotError()
{
$server = $this->createServerModel();
/** @var \Pterodactyl\Models\Allocation[] $allocations */
$allocation = factory(Allocation::class)->create(['node_id' => $server->node_id, 'server_id' => $server->id]);
$allocation2 = factory(Allocation::class)->create(['node_id' => $server->node_id]);
$this->daemonServerRepository->expects('setServer->update')->andReturnUndefined();
$this->getService()->handle($server, [
'add_allocations' => [$allocation2->id, $allocation2->id],
'remove_allocations' => [$allocation->id, $allocation->id],
]);
$this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => null]);
$this->assertDatabaseHas('allocations', ['id' => $allocation2->id, 'server_id' => $server->id]);
}
/**
* Test that any changes we made to the server or allocations are rolled back if there is an
* exception while performing any action.
*/
public function testThatUpdatesAreRolledBackIfExceptionIsEncountered()
{
$server = $this->createServerModel();
/** @var \Pterodactyl\Models\Allocation[] $allocations */
$allocation = factory(Allocation::class)->create(['node_id' => $server->node_id]);
$this->daemonServerRepository->expects('setServer->update')->andThrows(new DisplayException('Test'));
$this->expectException(DisplayException::class);
$this->getService()->handle($server, ['add_allocations' => [$allocation->id]]);
$this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => null]);
}
/**
* @return \Pterodactyl\Services\Servers\BuildModificationService
*/
private function getService()
{
return $this->app->make(BuildModificationService::class);
}
}

View file

@ -1,102 +0,0 @@
<?php
namespace Tests\Unit\Commands\Schedule;
use Mockery as m;
use Cake\Chronos\Chronos;
use Pterodactyl\Models\Task;
use Pterodactyl\Models\Schedule;
use Tests\Unit\Commands\CommandTestCase;
use Pterodactyl\Services\Schedules\ProcessScheduleService;
use Pterodactyl\Console\Commands\Schedule\ProcessRunnableCommand;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
class ProcessRunnableCommandTest extends CommandTestCase
{
/**
* @var \Pterodactyl\Console\Commands\Schedule\ProcessRunnableCommand
*/
protected $command;
/**
* @var \Pterodactyl\Services\Schedules\ProcessScheduleService
*/
protected $processScheduleService;
/**
* @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface
*/
protected $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
Chronos::setTestNow(Chronos::now());
$this->processScheduleService = m::mock(ProcessScheduleService::class);
$this->repository = m::mock(ScheduleRepositoryInterface::class);
$this->command = new ProcessRunnableCommand($this->processScheduleService, $this->repository);
}
/**
* Test that a schedule can be queued up correctly.
*/
public function testScheduleIsQueued()
{
$schedule = factory(Schedule::class)->make();
$schedule->tasks = collect([factory(Task::class)->make()]);
$this->repository->shouldReceive('getSchedulesToProcess')->with(Chronos::now()->toAtomString())->once()->andReturn(collect([$schedule]));
$this->processScheduleService->shouldReceive('handle')->with($schedule)->once()->andReturnNull();
$display = $this->runCommand($this->command);
$this->assertNotEmpty($display);
$this->assertStringContainsString(trans('command/messages.schedule.output_line', [
'schedule' => $schedule->name,
'hash' => $schedule->hashid,
]), $display);
}
/**
* If tasks is an empty collection, don't process it.
*/
public function testScheduleWithNoTasksIsNotProcessed()
{
$schedule = factory(Schedule::class)->make();
$schedule->tasks = collect([]);
$this->repository->shouldReceive('getSchedulesToProcess')->with(Chronos::now()->toAtomString())->once()->andReturn(collect([$schedule]));
$display = $this->runCommand($this->command);
$this->assertNotEmpty($display);
$this->assertStringNotContainsString(trans('command/messages.schedule.output_line', [
'schedule' => $schedule->name,
'hash' => $schedule->hashid,
]), $display);
}
/**
* If tasks isn't an instance of a collection, don't process it.
*/
public function testScheduleWithTasksObjectThatIsNotInstanceOfCollectionIsNotProcessed()
{
$schedule = factory(Schedule::class)->make(['tasks' => null]);
$this->repository->shouldReceive('getSchedulesToProcess')->with(Chronos::now()->toAtomString())->once()->andReturn(collect([$schedule]));
$display = $this->runCommand($this->command);
$this->assertNotEmpty($display);
$this->assertStringNotContainsString(trans('command/messages.schedule.output_line', [
'schedule' => $schedule->name,
'hash' => $schedule->hashid,
]), $display);
}
}

View file

@ -1201,6 +1201,13 @@
dependencies: dependencies:
query-string "*" query-string "*"
"@types/react-copy-to-clipboard@^4.3.0":
version "4.3.0"
resolved "https://registry.yarnpkg.com/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-4.3.0.tgz#8e07becb4f11cfced4bd36038cb5bdf5c2658be5"
integrity sha512-iideNPRyroENqsOFh1i2Dv3zkviYS9r/9qD9Uh3Z9NNoAAqqa2x53i7iGndGNnJFIo20wIu7Hgh77tx1io8bgw==
dependencies:
"@types/react" "*"
"@types/react-dom@^16.9.8": "@types/react-dom@^16.9.8":
version "16.9.8" version "16.9.8"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423"
@ -2460,6 +2467,13 @@ copy-descriptor@^0.1.0:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
copy-to-clipboard@^3:
version "3.3.1"
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae"
integrity sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==
dependencies:
toggle-selection "^1.0.6"
core-js-compat@^3.6.2: core-js-compat@^3.6.2:
version "3.6.5" version "3.6.5"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.5.tgz#2a51d9a4e25dfd6e690251aa81f99e3c05481f1c" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.5.tgz#2a51d9a4e25dfd6e690251aa81f99e3c05481f1c"
@ -5650,7 +5664,7 @@ promise-inflight@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
prop-types@^15.5.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: prop-types@^15.5.0, prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2" version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -5802,6 +5816,14 @@ react-async-script@^1.1.1:
hoist-non-react-statics "^3.3.0" hoist-non-react-statics "^3.3.0"
prop-types "^15.5.0" prop-types "^15.5.0"
react-copy-to-clipboard@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz#d82a437e081e68dfca3761fbd57dbf2abdda1316"
integrity sha512-/2t5mLMMPuN5GmdXo6TebFa8IoFxZ+KTDDqYhcDm0PhkgEzSxVvIX26G20s1EB02A4h2UZgwtfymZ3lGJm0OLg==
dependencies:
copy-to-clipboard "^3"
prop-types "^15.5.8"
"react-dom@npm:@hot-loader/react-dom": "react-dom@npm:@hot-loader/react-dom":
version "16.11.0" version "16.11.0"
resolved "https://registry.yarnpkg.com/@hot-loader/react-dom/-/react-dom-16.11.0.tgz#c0b483923b289db5431516f56ee2a69448ebf9bd" resolved "https://registry.yarnpkg.com/@hot-loader/react-dom/-/react-dom-16.11.0.tgz#c0b483923b289db5431516f56ee2a69448ebf9bd"
@ -7027,6 +7049,11 @@ to-regex@^3.0.1, to-regex@^3.0.2:
regex-not "^1.0.2" regex-not "^1.0.2"
safe-regex "^1.1.0" safe-regex "^1.1.0"
toggle-selection@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI=
toidentifier@1.0.0: toidentifier@1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"