Merge pull request #163 from Pterodactyl/develop

Merge develop into master
This commit is contained in:
Dane Everitt 2016-11-05 00:05:41 -04:00 committed by GitHub
commit b5a778549e
502 changed files with 6583 additions and 2145 deletions

View file

@ -3,6 +3,10 @@ APP_DEBUG=false
APP_KEY=SomeRandomString3232RandomString
APP_THEME=default
APP_TIMEZONE=UTC
APP_CLEAR_TASKLOG=720
APP_DELETE_MINUTES=10
CONSOLE_PUSH_FREQ=250
CONSOLE_PUSH_COUNT=10
DB_HOST=localhost
DB_PORT=3306
@ -12,7 +16,6 @@ DB_PASSWORD=secret
CACHE_DRIVER=file
SESSION_DRIVER=database
QUEUE_DRIVER=database
MAIL_DRIVER=smtp
MAIL_HOST=mailtrap.io
@ -25,3 +28,12 @@ MAIL_FROM=you@example.com
API_PREFIX=api
API_VERSION=v1
API_DEBUG=false
QUEUE_DRIVER=database
QUEUE_HIGH=high
QUEUE_STANDARD=standard
QUEUE_LOW=low
SQS_KEY=aws-public
SQS_SECRET=aws-secret
SQS_QUEUE_PREFIX=aws-queue-prefix

61
.github/ISSUE_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,61 @@
<!-- The checkboxes below can be clicked once you submit this report if you'd like -->
<!-- You can also use "- [x]" to mark it as checked. -->
## Product
Please check the corresponding boxes below for which products this is about.
- [ ] Panel
- [ ] Daemon
- [ ] Dockerfile(s) [Please list if so: __ ]
## Type
- [ ] Bug or Issue
- [ ] Feature Request
- [ ] Enhancement
- [ ] Other
<!-- You only need to fill out the information below if this is a bug report. -->
<!-- Please delete this line and everything below if this is NOT a bug report. -->
## What Happens
<!-- Please include a description of what is happening when you encounter this bug. -->
## How to Reproduce
<!-- Please provide us a list of step for how to reproduce this issue. -->
1. Step 1
2. Step 2
3. etc.
## Error Logs
<!-- Please include a paste output of the errors if they are logged. They can be found in: -->
<!-- Panel: /var/www/pterodactyl/html/storage/logs/ Daemon: /srv/daemon/logs -->
<!-- You can also paste them on https://gist.github.com and include their links below. -->
```
error logs
```
## System Information
#### Output of `uname -a`:
```
paste here
```
#### Output of `php -v` (if Panel):
```
paste here
```
#### Output of `node -v` (if Daemon):
```
paste here
```
#### Output of `docker info` and `docker -v` (if Daemon or Dockerfiles):
```
paste here
```

3
.gitignore vendored
View file

@ -8,3 +8,6 @@ composer.lock
Homestead.yaml
Vagrantfile
Vagrantfile
node_modules
.babelrc

29
.phraseapp.yml Normal file
View file

@ -0,0 +1,29 @@
phraseapp:
project_id: 94f8b39450cd749ae9c3cc0ab8cdb61d
file_format: laravel
push:
sources:
- file: ./resources/lang/<locale_code>/<tag>.php
pull:
targets:
- file: ./resources/lang/<locale_code>/auth.php
params:
tag: "auth"
- file: ./resources/lang/<locale_code>/base.php
params:
tag: "base"
- file: ./resources/lang/<locale_code>/pagination.php
params:
tag: "pagination"
- file: ./resources/lang/<locale_code>/passwords.php
params:
tag: "passwords"
- file: ./resources/lang/<locale_code>/server.php
params:
tag: "server"
- file: ./resources/lang/<locale_code>/strings.php
params:
tag: "strings"
- file: ./resources/lang/<locale_code>/validation.php
params:
tag: "validation"

View file

@ -3,7 +3,187 @@ 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.
## v0.4.1
## v0.5.0 (Bodacious Boreopterus)
After nearly a month in the works, version `v0.5.0` is finally here! 🎉
### Added
* Foreign keys are now enabled on all tables that the panel makes use of to prevent accidental data deletion when associated with other tables.
* Javascript changes to prevent crashing browsers when large quantities of data are sent over the websocket to the console. Includes a small popover message on the console to alert users that it is being throttled.
* Support for 'ARK: Survival Evolved' servers through the panel.
* Support for filtering servers within Admin CP to narrow down results by name, email, allocation, or defined fields.
* Setup scripts (user, mail, env) now support argument flags for use in containers and other non-terminal environments.
* New API endpoints for individual users to control their servers with at `/api/me/*`.
* Typeahead support for owner email when adding a new server.
* Scheduled command to clear out task log every month (configurable timespan).
* Support for allocating a FQDN as an allocation (panel will convert to IP and assign the FQDN as the alias automatically).
* Refresh files button in file manager to reload file listing without full page refresh.
* Added support for file copying through the file manager. [#127](https://github.com/Pterodactyl/Panel/issues/127)
* Creating new files and folders directly from the right-click dropdown menu in the file manager.
* Support for setting custom `user_id` when using the API to create users.
* Support for creating a new server through the API by passing a user ID rather than an email.
* Passing `?daemon=true` flag to [`/api/servers/:id`](https://pterodactyl.readme.io/v0.5.0/reference#single-server) will return the daemon stats as well as the `daemon_token` if using HTTPS.
* Small check for current node status that shows up to the left of the name when viewing a listing of all nodes.
* Support for creating server without having to assign a node and allocation manually. Simply select the checkbox or pass `auto_deploy=true` to the API to auto-select a node and allocation given a location.
* Support for setting IP Aliases through the panel on the node overview page. Also cleaned up allocation removal.
* Support for renaming files through the panel's file mananger.
### Changed
* Servers are now queued for deletion to allow for cancellation of deletion, as well as run in the background to speed up page loading.
* Switched to new graphing library to make graphs less... broken.
* Rebuild triggers are only sent to the node if there is actually something changed that requires a rebuild.
* Dependencies are now hard-coded into the `composer.json` file to prevent users installing slightly different versions with different features or bugs.
* Server related tasks now use the lowest priorty queue to prevent clogging the pipes when there are more important tasks to be run by the panel.
* Dates displayed in the file manager are now more user friendly.
* Creating a user, server, or node now returns `HTTP/1.1 200` and a JSON element with the user/server/node's ID.
* Environment setting script is much more user friendly and does not require an excessive amount of clicking and typing.
* File upload method switched from BinaryJS to Socket.io implementation to fix bugs as well as be a little speedier and allow upload throttling.
* `Server::getbyUUID()` now accepts either the `uuidShort` or full-length `uuid` for server identification.
* API keys are tied to individual users and no longer created through the Admin CP.
* **ALL** API routes previously returning paginated result sets, or result sets nested inside a descriptive block (e.g. `servers:`) have been changed to return a single array of all associated items. Please see the [updated documentation](https://pterodactyl.readme.io/v0.5.0/reference) for how this change might effect your API use.
* API route for [`/api/users/:id`](https://pterodactyl.readme.io/v0.5.0/reference#single-user) now includes an array of all servers the user is set as the owner of.
* Prevent clicking server start button until server is completely off, not just stopping.
* Upon successful creation of a node it will redirect to the allocation tab and display a clearer message to add allocations.
* Trying to add a new node if no location exists redirects user to location management page and alerts them to add a location first.
* `Server\AjaxController@postSetConnection` is now `Server\AjaxController@postSetPrimary` and accepts one post parameter of `allocation` rather than a combined `ip:port` value.
* Port allocations on server view are now cleaner and should make more sense.
* Improved File Manager
* Rewritten Javascript to load, rename, and handle other file actions.
* Uses Ace Editor for editing files rather than a non-formatted textarea
* File actions that were previously icons to the right are now contained in a menu that appears when right-clicking a file or folder.
### Fixed
* Fixes bug where resetting a user password through the login form would not hold passwords to the same requirements as the rest of the panel (mixed case and at least one numeric character).
* Fixes bug where no error would be displayed when adding a new server with an invalid owner email.
* Fixes a bug that could allow an admin to delete the default allocation for a server causing all sorts of issues.
* Databases assigned to a server are now actually deleted when a server is removed.
* Server overview listing the location short-code as the name of the node.
* Server task manager only sending commands every 5 minutes at the quickest.
* Fixes additional port allocation from removing the wrong row when clicking 'x'.
* Updated Socket.io client file to version `1.5.0` to match the latest release. Correlates with setting hard dependencies in the Daemon.
* Team Fortress named 'Insurgency' in panel in database seeder. ([#96](https://github.com/Pterodactyl/Panel/issues/96), PR by [@MeltedLux](https://github.com/MeltedLux))
* Server allocation listing display now showing the connection IP unless an alias was assigned.
* Fixed bug where node allocation would appear to be successful but actual encounter an error. Made it cleared how to enter ports.
* Fixes display where an extra space was added to the end of SFTP passwords when they were copied from the panel. [#116](https://github.com/Pterodactyl/Panel/issues/116), thanks [@OrangeJuiced](https://github.com/OrangeJuiced)
* Fixes a bug that prevented viewing database servers if not assigned to a node.
* Checkboxes previously not displayed checkmarks are now fixed.
### Fixed (bugs from v0.5.0-rc.2)
* Fixes a bug causing password resets to fail for server databases.
* Fixes a bug during installation that would prevent the 'Ark: Survival Evolved' service option from being added to the panel unless it was an update.
* Fixes constant scrolling to bottom of console; console now only scrolls to the bottom on new data.
### Removed
* Removed active session management table displaying the last location of a session.
* Removed online player listing due to inconsistency in query library and an assortment of query related bugs. This will return in future versions when we get it working correctly.
## v0.5.0-rc.2 (Bodacious Boreopterus)
### Fixed
* Fixes a bug that would cause MySQL errors when attempting to install the panel rather than upgrading.
## v0.5.0-rc.1 (Bodacious Boreopterus)
### Added
* Foreign keys are now enabled on all tables that the panel makes use of to prevent accidental data deletion when associated with other tables.
* Javascript changes to prevent crashing browsers when large quantities of data are sent over the websocket to the console. Includes a small popover message on the console to alert users that it is being throttled.
* Support for 'ARK: Survival Evolved' servers through the panel.
### Fixed
* Fixes bug where resetting a user password through the login form would not hold passwords to the same requirements as the rest of the panel (mixed case and at least one numeric character).
* Fixes misnamed environment variable for Bungeecord Servers (`BUNGE_VERSION` -> `BUNGEE_VERSION`).
* Fixes bug where no error would be displayed when adding a new server with an invalid owner email.
* Fixes a bug that could allow an admin to delete the default allocation for a server causing all sorts of issues.
* Databases assigned to a server are now actually deleted when a server is removed.
* Fixes file uploads being improperly throttled.
### Changed
* Servers are now queued for deletion to allow for cancellation of deletion, as well as run in the background to speed up page loading.
* Switched to new graphing library to make graphs less... broken.
* Rebuild triggers are only sent to the node if there is actually something changed that requires a rebuild.
* Dependencies are now hard-coded into the `composer.json` file to prevent users installing slightly different versions with different features or bugs.
* Server related tasks now use the lowest priorty queue to prevent clogging the pipes when there are more important tasks to be run by the panel.
* Decompressing files now shows a pop-over box that does not dismiss until it is complete.
* Dates displayed in the file manager are now more user friendly.
### Removed
* Removed online player listing due to inconsistency in query library and an assortment of query related bugs. This will return in future versions when we get it working correctly.
## v0.5.0-pre.3 (Bodacious Boreopterus)
### Added
* Return node configuration from remote API by using `/api/nodes/{id}/config` endpoint. Only accepts SSL connections.
* Support for filtering servers within Admin CP to narrow down results by name, email, allocation, or defined fields.
* Setup scripts (user, mail, env) now support argument flags for use in containers and other non-terminal environments.
* New API endpoints for individual users to control their servers with at `/api/me/*`.
* Typeahead support for owner email when adding a new server.
* Scheduled command to clear out task log every month (configurable timespan).
* Support for allocating a FQDN as an allocation (panel will convert to IP and assign the FQDN as the alias automatically).
* Refresh files button in file manager to reload file listing without full page refresh.
### Changed
* Creating a user, server, or node now returns `HTTP/1.1 200` and a JSON element with the user/server/node's ID.
* Environment setting script is much more user friendly and does not require an excessive amount of clicking and typing.
* File upload method switched from BinaryJS to Socket.io implementation to fix bugs as well as be a little speedier and allow upload throttling.
* `Server::getbyUUID()` now accepts either the `uuidShort` or full-length `uuid` for server identification.
* API keys are tied to individual users and no longer created through the Admin CP.
### Fixed
* Server overview listing the location short-code as the name of the node.
* Server task manager only sending commands every 5 minutes at the quickest.
* Fixes additional port allocation from removing the wrong row when clicking 'x'.
## v0.5.0-pre.2 (Bodacious Boreopterus)
### Added
* Added support for file copying through the file manager. [#127](https://github.com/Pterodactyl/Panel/issues/127)
* Creating new files and folders directly from the right-click dropdown menu in the file manager.
* Support for setting custom `user_id` when using the API to create users.
* Support for creating a new server through the API by passing a user ID rather than an email.
* Passing `?daemon=true` flag to [`/api/servers/:id`](https://pterodactyl.readme.io/v0.5.0/reference#single-server) will return the daemon stats as well as the `daemon_token` if using HTTPS.
* Small check for current node status that shows up to the left of the name when viewing a listing of all nodes.
### Changed
* Support for sub-folders within the `getJavascript()` route for servers.
* **ALL** API routes previously returning paginated result sets, or result sets nested inside a descriptive block (e.g. `servers:`) have been changed to return a single array of all associated items. Please see the [updated documentation](https://pterodactyl.readme.io/v0.5.0/reference) for how this change might effect your API use.
* API route for [`/api/users/:id`](https://pterodactyl.readme.io/v0.5.0/reference#single-user) now includes an array of all servers the user is set as the owner of.
### Fixed
* File manager would do multiple up-down-up-down loading actions if you escaped renaming a file. Fixed the binding issue. [#122](https://github.com/Pterodactyl/Panel/issues/122)
* File manager actions would not trigger properly if text in a row was used to right-click from.
* File manager rename field would not disappear when pressing the escape key in chrome. [#121](https://github.com/Pterodactyl/Panel/issues/121)
* Fixes bug where server image assigned was not being saved to the database.
* Fixes instances where selecting auto-deploy would not hide the node selection dropdown.
* Fixes bug in auto-deployment that would throw a `ModelNotFoundException` if the location passed was not valid. Not normally an issue in the panel, but caused display issues for the API.
* Updated Socket.io client file to version `1.5.0` to match the latest release. Correlates with setting hard dependencies in the Daemon.
## v0.5.0-pre.1 (Bodacious Boreopterus)
### Added
* Support for creating server without having to assign a node and allocation manually. Simply select the checkbox or pass `auto_deploy=true` to the API to auto-select a node and allocation given a location.
* Support for setting IP Aliases through the panel on the node overview page. Also cleaned up allocation removal.
* Support for renaming files through the panel's file mananger.
### Changed
* Prevent clicking server start button until server is completely off, not just stopping.
* Upon successful creation of a node it will redirect to the allocation tab and display a clearer message to add allocations.
* Trying to add a new node if no location exists redirects user to location management page and alerts them to add a location first.
* `Server\AjaxController@postSetConnection` is now `Server\AjaxController@postSetPrimary` and accepts one post parameter of `allocation` rather than a combined `ip:port` value.
* Port allocations on server view are now cleaner and should make more sense.
* Improved File Manager
* Rewritten Javascript to load, rename, and handle other file actions.
* Uses Ace Editor for editing files rather than a non-formatted textarea
* File actions that were previously icons to the right are now contained in a menu that appears when right-clicking a file or folder.
### Fixed
* Team Fortress named 'Insurgency' in panel in database seeder. ([#96](https://github.com/Pterodactyl/Panel/issues/96), PR by [@MeltedLux](https://github.com/MeltedLux))
* Server allocation listing display now showing the connection IP unless an alias was assigned.
* Fixed bug where node allocation would appear to be successful but actual encounter an error. Made it cleared how to enter ports.
* Fixes display where an extra space was added to the end of SFTP passwords when they were copied from the panel. [#116](https://github.com/Pterodactyl/Panel/issues/116), thanks [@OrangeJuiced](https://github.com/OrangeJuiced)
### Removed
* Removed active session management table displaying the last location of a session.
## v0.4.1 (Articulate Aerotitan)
### Changed
* Overallocate fields are now auto-filled with a value of `0`
@ -13,7 +193,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines.
* Server link in navbar directed to 404 link (PR by [@Randomfish132](https://github.com/Randomfish132))
* Composer fails to finish ([#92](https://github.com/Pterodactyl/Panel/issues/92), PR by [@schrej](https://github.com/schrej), thanks [@parkervcp](https://github.com/parkervcp))
## v0.4.0
## v0.4.0 (Arty Aerodactylus)
### Added
* Task scheduler supporting customized CRON syntax or dropdown selected options. (currently only support command and power options)

View file

@ -1,5 +1,5 @@
## Pterodactyl Panel
Pterodactyl is the free game server management panel designed by users, for users. Featuring support for Vanilla Minecraft, Spigot, Source Dedicated Servers, BungeeCord, and many more. Pterodactyl is built on the `Laravel PHP Framework (v5.2)`.
Pterodactyl is the free game server management panel designed by users, for users. Featuring support for Vanilla Minecraft, Spigot, Source Dedicated Servers, BungeeCord, and many more. Pterodactyl is built on the `Laravel PHP Framework (v5.3)`.
## Support & Documentation
Support for using Pterodactyl can be found on our [wiki](https://github.com/Pterodactyl/Panel/wiki) or on our [Discord chat](https://discord.gg/0gYt8oU8QOkDhKLS).
@ -28,6 +28,11 @@ SOFTWARE.
```
### Credits
![](http://static.s3.pterodactyl.io/PhraseApp-parrot.png)
A huge thanks to [PhraseApp](https://phraseapp.com) who provide us the software to help translate this project.
Ace Editor - [license](https://github.com/ajaxorg/ace/blob/master/LICENSE) - [homepage](https://ace.c9.io)
Animate.css - [license](https://github.com/daneden/animate.css/blob/master/LICENSE) - [homepage](http://daneden.github.io/animate.css/)
Async.js - [license](https://github.com/caolan/async/blob/master/LICENSE) - [homepage](https://github.com/caolan/async/)
@ -38,22 +43,26 @@ Bootstrap - [license](https://github.com/twbs/bootstrap/blob/master/LICENSE) - [
BootStrap Notify - [license](https://github.com/mouse0270/bootstrap-notify/blob/master/LICENSE) - [homepage](http://bootstrap-notify.remabledesigns.com)
D3.js - [license](https://github.com/mbostock/d3/blob/master/LICENSE) - [homepage](https://d3js.org/)
Chart.js - [license](https://github.com/chartjs/Chart.js/blob/master/LICENSE.md) - [homepage](http://www.chartjs.org)
FontAwesome - [license](http://fontawesome.io/license/) - [homepage](http://fontawesome.io)
FontAwesome Animations - [license](https://github.com/l-lin/font-awesome-animation#license) - [homepage](https://github.com/l-lin/font-awesome-animation)
FuelUX - [license](https://github.com/ExactTarget/fuelux/blob/master/LICENSE) - [homepage](http://getfuelux.com)
jQuery - [license](https://github.com/jquery/jquery/blob/master/LICENSE.txt) - [homepage](http://jquery.com)
jQuery Terminal - [license](https://github.com/jcubic/jquery.terminal/blob/master/LICENSE) - [homepage](http://terminal.jcubic.pl)
MetricsGraphics.js - [license](https://github.com/mozilla/metrics-graphics/blob/master/LICENSE) - [homepage](http://metricsgraphicsjs.org/)
Lodash - [license](https://github.com/lodash/lodash/blob/master/LICENSE) - [homepage](https://lodash.com/)
Socket.io - [license](https://github.com/socketio/socket.io/blob/master/LICENSE) - [homepage](http://socket.io)
SweetAlert - [license](https://github.com/t4t5/sweetalert/blob/master/LICENSE) - [homepage](http://t4t5.github.io/sweetalert/)
Typeahead — [license](https://github.com/bassjobsen/Bootstrap-3-Typeahead/blob/master/bootstrap3-typeahead.js) — [homepage](https://github.com/bassjobsen/Bootstrap-3-Typeahead)
### Additional License Information
Some Javascript and CSS used within the panel is licensed under a `MIT` or `Apache 2.0`. Please check their respective header files for more information.

View file

@ -0,0 +1,83 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Console\Commands;
use DB;
use Carbon;
use Pterodactyl\Models;
use Illuminate\Console\Command;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Pterodactyl\Jobs\SendScheduledTask;
class ClearTasks extends Command
{
use DispatchesJobs;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'pterodactyl:tasks:clearlog';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clears old log entires (> 2 months) from the last log.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$entries = Models\TaskLog::where('run_time', '<=', Carbon::now()->subHours(env('APP_CLEAR_TASKLOG', 720))->toAtomString())->get();
$this->info(sprintf('Preparing to delete %d old task log entries.', count($entries)));
$bar = $this->output->createProgressBar(count($entries));
foreach ($entries as &$entry) {
$entry->delete();
$bar->advance();
}
$bar->finish();
$this->info("\nFinished deleting old logs.");
}
}

View file

@ -35,7 +35,10 @@ class MakeUser extends Command
*
* @var string
*/
protected $signature = 'pterodactyl:user';
protected $signature = 'pterodactyl:user
{--email= : Email address to use for this account.}
{--password= : Password to assign to the user.}
{--admin= : Boolean flag for if user should be an admin.}';
/**
* The console command description.
@ -61,15 +64,15 @@ class MakeUser extends Command
*/
public function handle()
{
$email = $this->ask('Email');
$password = $this->secret('Password');
$password_confirmation = $this->secret('Confirm Password');
$email = is_null($this->option('email')) ? $this->ask('Email') : $this->option('email');
$password = is_null($this->option('password')) ? $this->secret('Password') : $this->option('password');
$password_confirmation = is_null($this->option('password')) ? $this->secret('Confirm Password') : $this->option('password');
if ($password !== $password_confirmation) {
return $this->error('The passwords provided did not match!');
}
$admin = $this->confirm('Is this user a root administrator?');
$admin = is_null($this->option('admin')) ? $this->confirm('Is this user a root administrator?') : $this->option('admin');
try {
$user = new UserRepository;

View file

@ -74,7 +74,7 @@ class RunTasks extends Command
foreach ($tasks as &$task) {
$bar->advance();
$this->dispatch(new SendScheduledTask(Models\Server::findOrFail($task->server), $task));
$this->dispatch((new SendScheduledTask(Models\Server::findOrFail($task->server), $task))->onQueue(env('QUEUE_LOW', 'low')));
}
$bar->finish();

View file

@ -32,7 +32,13 @@ class UpdateEmailSettings extends Command
*
* @var string
*/
protected $signature = 'pterodactyl:mail';
protected $signature = 'pterodactyl:mail
{--driver=}
{--email=}
{--host=}
{--port=}
{--username=}
{--password=}';
/**
* The console command description.
@ -92,35 +98,35 @@ class UpdateEmailSettings extends Command
'Postmark Transactional Email Service'
]
]);
$variables['MAIL_DRIVER'] = $this->choice('Which email driver would you like to use?', [
$variables['MAIL_DRIVER'] = is_null($this->option('driver')) ? $this->choice('Which email driver would you like to use?', [
'smtp',
'mail',
'mailgun',
'mandrill',
'postmark'
]);
]) : $this->option('driver');
switch ($variables['MAIL_DRIVER']) {
case 'smtp':
$variables['MAIL_HOST'] = $this->ask('SMTP Host (e.g smtp.google.com)');
$variables['MAIL_PORT'] = $this->anticipate('SMTP Host Port (e.g 587)', ['587']);
$variables['MAIL_USERNAME'] = $this->ask('SMTP Username');
$variables['MAIL_PASSWORD'] = $this->secret('SMTP Password');
$variables['MAIL_HOST'] = is_null($this->option('host')) ? $this->ask('SMTP Host (e.g smtp.google.com)') : $this->option('host');
$variables['MAIL_PORT'] = is_null($this->option('port')) ? $this->anticipate('SMTP Host Port (e.g 587)', ['587']) : $this->option('port');
$variables['MAIL_USERNAME'] = is_null($this->option('username')) ? $this->ask('SMTP Username') : $this->option('password');
$variables['MAIL_PASSWORD'] = is_null($this->option('password')) ? $this->secret('SMTP Password') : $this->option('password');
break;
case 'mail':
break;
case 'mailgun':
$variables['MAILGUN_DOMAIN'] = $this->ask('Mailgun Domain');
$variables['MAILGUN_KEY'] = $this->ask('Mailgun Key');
$variables['MAILGUN_DOMAIN'] = is_null($this->option('host')) ? $this->ask('Mailgun Domain') : $this->option('host');
$variables['MAILGUN_KEY'] = is_null($this->option('username')) ? $this->ask('Mailgun Key') : $this->option('username');
break;
case 'mandrill':
$variables['MANDRILL_SECRET'] = $this->ask('Mandrill Secret');
$variables['MANDRILL_SECRET'] = is_null($this->option('username')) ? $this->ask('Mandrill Secret') : $this->option('username');
break;
case 'postmark':
$variables['MAIL_DRIVER'] = 'smtp';
$variables['MAIL_HOST'] = 'smtp.postmarkapp.com';
$variables['MAIL_PORT'] = 587;
$variables['MAIL_USERNAME'] = $this->ask('Postmark API Token');
$variables['MAIL_USERNAME'] = is_null($this->option('username')) ? $this->ask('Postmark API Token') : $this->option('username');
$variables['MAIL_PASSWORD'] = $variables['MAIL_USERNAME'];
break;
default:
@ -129,7 +135,7 @@ class UpdateEmailSettings extends Command
break;
}
$variables['MAIL_FROM'] = $this->ask('Email address emails should originate from');
$variables['MAIL_FROM'] = is_null($this->option('email')) ? $this->ask('Email address emails should originate from') : $this->option('email');
$variables['MAIL_ENCRYPTION'] = 'tls';
$bar = $this->output->createProgressBar(count($variables));

View file

@ -33,7 +33,14 @@ class UpdateEnvironment extends Command
*
* @var string
*/
protected $signature = 'pterodactyl:env';
protected $signature = 'pterodactyl:env
{--dbhost=}
{--dbport=}
{--dbname=}
{--dbuser=}
{--dbpass=}
{--url=}
{--timezone=}';
/**
* The console command description.
@ -69,40 +76,62 @@ class UpdateEnvironment extends Command
$envContents = file_get_contents($file);
$this->info('Simply leave blank and press enter to fields that you do not wish to update.');
if (!env('SERVICE_AUTHOR', false)) {
$this->info('No service author set, setting one now.');
$variables['SERVICE_AUTHOR'] = env('SERVICE_AUTHOR', (string) Uuid::generate(4));
}
// DB info
if($this->confirm('Update database host? [' . env('DB_HOST') . ']')) {
$variables['DB_HOST'] = $this->anticipate('Database Host (usually \'localhost\' or \'127.0.0.1\')', [ 'localhost', '127.0.0.1', env('DB_HOST') ]);
if (!env('QUEUE_STANDARD', false) || !env('QUEUE_DRIVER', false)) {
$this->info('Setting default queue settings.');
$variables['QUEUE_DRIVER'] = env('QUEUE_DRIVER', 'database');
$variables['QUEUE_HIGH'] = env('QUEUE_HIGH', 'high');
$variables['QUEUE_STANDARD'] = env('QUEUE_STANDARD', 'standard');
$variables['QUEUE_LOW'] = env('QUEUE_LOW', 'low');
}
if($this->confirm('Update database port? [' . env('DB_PORT') . ']')) {
$variables['DB_PORT'] = $this->anticipate('Database Port', [ 3306, env('DB_PORT') ]);
if (is_null($this->option('dbhost'))) {
$variables['DB_HOST'] = $this->anticipate('Database Host', [ 'localhost', '127.0.0.1', env('DB_HOST') ], env('DB_HOST'));
} else {
$variables['DB_HOST'] = $this->option('dbhost');
}
if($this->confirm('Update database name? [' . env('DB_DATABASE') . ']')) {
$variables['DB_DATABASE'] = $this->anticipate('Database Name', [ 'pterodactyl', 'homestead', ENV('DB_DATABASE') ]);
if (is_null($this->option('dbport'))) {
$variables['DB_PORT'] = $this->anticipate('Database Port', [ 3306, env('DB_PORT') ], env('DB_PORT'));
} else {
$variables['DB_PORT'] = $this->option('dbport');
}
if($this->confirm('Update database username? [' . env('DB_USERNAME') . ']')) {
$variables['DB_USERNAME'] = $this->anticipate('Database Username', [ env('DB_USERNAME') ]);
if (is_null($this->option('dbname'))) {
$variables['DB_DATABASE'] = $this->anticipate('Database Name', [ 'pterodactyl', 'homestead', ENV('DB_DATABASE') ], env('DB_DATABASE'));
} else {
$variables['DB_DATABASE'] = $this->option('dbname');
}
if($this->confirm('Update database password?')) {
$variables['DB_PASSWORD'] = $this->secret('Database User\'s Password');
if (is_null($this->option('dbuser'))) {
$variables['DB_USERNAME'] = $this->anticipate('Database Username', [ ENV('DB_DATABASE') ], env('DB_USERNAME'));
} else {
$variables['DB_USERNAME'] = $this->option('dbuser');
}
// Other Basic Information
if($this->confirm('Update panel URL? [' . env('APP_URL') . ']')) {
$variables['APP_URL'] = $this->anticipate('Enter your current panel URL (include http or https).', [ env('APP_URL', 'http://localhost') ]);
if (is_null($this->option('dbpass'))) {
$this->line('The Database Password field is required; you cannot hit enter and use a default value.');
$variables['DB_PASSWORD'] = $this->secret('Database User Password');
} else {
$variables['DB_PASSWORD'] = $this->option('dbpass');
}
if($this->confirm('Update panel timezone? [' . env('APP_TIMEZONE') . ']')) {
if (is_null($this->option('url'))) {
$variables['APP_URL'] = $this->ask('Panel URL', env('APP_URL'));
} else {
$variables['APP_URL'] = $this->option('url');
}
if (is_null($this->option('timezone'))) {
$this->line('The timezone should match one of the supported timezones according to http://php.net/manual/en/timezones.php');
$variables['APP_TIMEZONE'] = $this->anticipate('Enter the timezone for this panel to run with', \DateTimeZone::listIdentifiers(\DateTimeZone::ALL));
$variables['APP_TIMEZONE'] = $this->anticipate('Panel Timezone', \DateTimeZone::listIdentifiers(\DateTimeZone::ALL), env('APP_TIMEZONE'));
} else {
$variables['APP_TIMEZONE'] = $this->option('timezone');
}
$bar = $this->output->createProgressBar(count($variables));

View file

@ -18,6 +18,7 @@ class Kernel extends ConsoleKernel
\Pterodactyl\Console\Commands\ShowVersion::class,
\Pterodactyl\Console\Commands\UpdateEnvironment::class,
\Pterodactyl\Console\Commands\RunTasks::class,
\Pterodactyl\Console\Commands\ClearTasks::class,
\Pterodactyl\Console\Commands\ClearServices::class,
\Pterodactyl\Console\Commands\UpdateEmailSettings::class,
];
@ -30,6 +31,7 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('pterodactyl:tasks')->everyFiveMinutes()->withoutOverlapping();
$schedule->command('pterodactyl:tasks')->everyMinute()->withoutOverlapping();
$schedule->command('pterodactyl:tasks:clearlog')->twiceDaily(3, 15);
}
}

View file

@ -0,0 +1,44 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Events;
use Illuminate\Queue\SerializesModels;
class ServerDeleted
{
use SerializesModels;
public $server;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($id)
{
$this->server = $id;
}
}

View file

@ -58,7 +58,7 @@ class LocationController extends BaseController
$location->nodes = explode(',', $location->nodes);
}
return $locations;
return $locations->toArray();
}
}

View file

@ -62,8 +62,7 @@ class NodeController extends BaseController
*/
public function list(Request $request)
{
$nodes = Models\Node::paginate(50);
return $this->response->paginator($nodes, new NodeTransformer);
return Models\Node::all()->toArray();
}
/**
@ -86,7 +85,7 @@ class NodeController extends BaseController
* 'daemonSFTP' => 2022,
* 'daemonListen' => 8080
* }, headers={"Authorization": "Bearer <jwt-token>"}),
* @Response(201),
* @Response(200),
* @Response(422, body={
* "message": "A validation error occured.",
* "errors": {},
@ -103,9 +102,7 @@ class NodeController extends BaseController
try {
$node = new NodeRepository;
$new = $node->create($request->all());
return $this->response->created(route('api.nodes.view', [
'id' => $new
]));
return [ 'id' => $new ];
} catch (DisplayValidationException $ex) {
throw new ResourceException('A validation error occured.', json_decode($ex->getMessage(), true));
} catch (DisplayException $ex) {
@ -130,23 +127,23 @@ class NodeController extends BaseController
*/
public function view(Request $request, $id, $fields = null)
{
$query = Models\Node::where('id', $id);
$node = Models\Node::where('id', $id);
if (!is_null($request->input('fields'))) {
foreach(explode(',', $request->input('fields')) as $field) {
if (!empty($field)) {
$query->addSelect($field);
$node->addSelect($field);
}
}
}
try {
if (!$query->first()) {
if (!$node->first()) {
throw new NotFoundHttpException('No node by that ID was found.');
}
return [
'node' => $query->first(),
'node' => $node->first(),
'allocations' => [
'assigned' => Models\Allocation::where('node', $id)->whereNotNull('assigned_to')->get(),
'unassigned' => Models\Allocation::where('node', $id)->whereNull('assigned_to')->get()
@ -159,6 +156,59 @@ class NodeController extends BaseController
}
}
public function config(Request $request, $id)
{
if (!$request->secure()) {
throw new BadRequestHttpException('This API route can only be accessed using a secure connection.');
}
$node = Models\Node::where('id', $id)->first();
if (!$node) {
throw new NotFoundHttpException('No node by that ID was found.');
}
return [
'web' => [
'listen' => $node->daemonListen,
'ssl' => [
'enabled' => ($node->scheme === 'https'),
'certificate' => '/etc/certs/' . $node->fqdn . '/fullchain.pem',
'key' => '/etc/certs/' . $node->fqdn . '/privkey.pem'
]
],
'docker' => [
'socket' => '/var/run/docker.sock',
'autoupdate_images' => true
],
'sftp' => [
'path' => $node->daemonBase,
'port' => (int) $node->daemonSFTP,
'container' => '0x0000'
],
'logger' => [
'path' => 'logs/',
'src' => false,
'level' => 'info',
'period' => '1d',
'count' => 3
],
'remote' => [
'download' => route('remote.download'),
'installed' => route('remote.install')
],
'uploads' => [
'maximumSize' => 100000000
],
'keys' => [
$node->daemonSecret
],
'query' => [
'kill_on_fail' => true,
'fail_limit' => 3
]
];
}
/**
* List all Node Allocations
*
@ -170,11 +220,11 @@ class NodeController extends BaseController
*/
public function allocations(Request $request)
{
$allocations = Models\Allocation::paginate(100);
$allocations = Models\Allocation::all();
if ($allocations->count() < 1) {
throw new NotFoundHttpException('No allocations have been created.');
}
return $this->response->paginator($allocations, new AllocationTransformer);
return $allocations;
}
/**

View file

@ -62,8 +62,7 @@ class ServerController extends BaseController
*/
public function list(Request $request)
{
$servers = Models\Server::paginate(50);
return $this->response->paginator($servers, new ServerTransformer);
return Models\Server::all()->toArray();
}
/**
@ -78,9 +77,7 @@ class ServerController extends BaseController
try {
$server = new ServerRepository;
$new = $server->create($request->all());
return $this->response->created(route('api.servers.view', [
'id' => $new
]));
return [ 'id' => $new ];
} catch (DisplayValidationException $ex) {
throw new ResourceException('A validation error occured.', json_decode($ex->getMessage(), true));
} catch (DisplayException $ex) {
@ -120,9 +117,38 @@ class ServerController extends BaseController
if (!$query->first()) {
throw new NotFoundHttpException('No server by that ID was found.');
}
return $query->first();
// Requested Daemon Stats
$server = $query->first();
if ($request->input('daemon') === 'true') {
$node = Models\Node::findOrFail($server->node);
$client = Models\Node::guzzleRequest($node->id);
$response = $client->request('GET', '/servers', [
'headers' => [
'X-Access-Token' => $node->daemonSecret
]
]);
// Only return the daemon token if the request is using HTTPS
if ($request->secure()) {
$server->daemon_token = $server->daemonSecret;
}
$server->daemon = json_decode($response->getBody())->{$server->uuid};
return $server->toArray();
}
return $server->toArray();
} catch (NotFoundHttpException $ex) {
throw $ex;
} catch (\GuzzleHttp\Exception\TransferException $ex) {
// Couldn't hit the daemon, return what we have though.
$server->daemon = [
'error' => 'There was an error encountered while attempting to connect to the remote daemon.'
];
return $server->toArray();
} catch (\Exception $ex) {
throw new BadRequestHttpException('There was an issue with the fields passed in the request.');
}

View file

@ -43,7 +43,7 @@ class ServiceController extends BaseController
public function list(Request $request)
{
return Models\Service::all();
return Models\Service::all()->toArray();
}
public function view(Request $request, $id)

View file

@ -0,0 +1,58 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Http\Controllers\API\User;
use Auth;
use Dingo;
use Pterodactyl\Models;
use Illuminate\Http\Request;
use Pterodactyl\Http\Controllers\API\BaseController;
class InfoController extends BaseController
{
public function me(Request $request)
{
$servers = Models\Server::getUserServers();
$response = [];
foreach($servers as &$server) {
$response = array_merge($response, [[
'id' => $server->uuidShort,
'uuid' => $server->uuid,
'name' => $server->name,
'node' => $server->nodeName,
'ip' => [
'set' => $server->ip,
'alias' => $server->ip_alias
],
'port' => $server->port,
'service' => $server->a_serviceName,
'option' => $server->a_serviceOptionName
]]);
}
return $response;
}
}

View file

@ -0,0 +1,117 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Http\Controllers\API\User;
use Auth;
use Log;
use Pterodactyl\Models;
use Illuminate\Http\Request;
use Pterodactyl\Http\Controllers\API\BaseController;
class ServerController extends BaseController
{
public function info(Request $request, $uuid)
{
$server = Models\Server::getByUUID($uuid);
$node = Models\Node::findOrFail($server->node);
$client = Models\Node::guzzleRequest($node->id);
try {
$response = $client->request('GET', '/server', [
'headers' => [
'X-Access-Token' => $server->daemonSecret,
'X-Access-Server' => $server->uuid
]
]);
$json = json_decode($response->getBody());
$daemon = [
'status' => $json->status,
'stats' => $json->proc,
'query' => $json->query
];
} catch (\Exception $ex) {
$daemon = [
'error' => 'An error was encountered while trying to connect to the daemon to collece information. It might be offline.'
];
Log::error($ex);
}
$allocations = Models\Allocation::select('id', 'ip', 'port', 'ip_alias as alias')->where('assigned_to', $server->id)->get();
foreach($allocations as &$allocation) {
$allocation->default = ($allocation->id === $server->allocation);
unset($allocation->id);
}
return [
'uuidShort' => $server->uuidShort,
'uuid' => $server->uuid,
'name' => $server->name,
'node' => $node->name,
'limits' => [
'memory' => $server->memory,
'swap' => $server->swap,
'disk' => $server->disk,
'io' => $server->io,
'cpu' => $server->cpu,
'oom_disabled' => (bool) $server->oom_disabled
],
'allocations' => $allocations,
'sftp' => [
'username' => (Auth::user()->can('view-sftp', $server)) ? $server->username : null
],
'daemon' => [
'token' => ($request->secure()) ? $server->daemonSecret : false,
'response' => $daemon
]
];
}
public function power(Request $request, $uuid)
{
$server = Models\Server::getByUUID($uuid);
$node = Models\Node::getByID($server->node);
$client = Models\Node::guzzleRequest($server->node);
Auth::user()->can('power-' . $request->input('action'), $server);
$res = $client->request('PUT', '/server/power', [
'headers' => [
'X-Access-Server' => $server->uuid,
'X-Access-Token' => $server->daemonSecret
],
'exceptions' => false,
'json' => [
'action' => $request->input('action')
]
]);
if ($res->getStatusCode() !== 204) {
return $this->response->error(json_decode($res->getBody())->error, $res->getStatusCode());
}
return $this->response->noContent();
}
}

View file

@ -62,8 +62,7 @@ class UserController extends BaseController
*/
public function list(Request $request)
{
$users = Models\User::paginate(50);
return $this->response->paginator($users, new UserTransformer);
return Models\User::all()->toArray();
}
/**
@ -95,7 +94,12 @@ class UserController extends BaseController
if (!$query->first()) {
throw new NotFoundHttpException('No user by that ID was found.');
}
return $query->first();
$user = $query->first();
$userArray = $user->toArray();
$userArray['servers'] = Models\Server::select('id', 'uuid', 'node', 'suspended')->where('owner', $user->id)->get();
return $userArray;
} catch (NotFoundHttpException $ex) {
throw $ex;
} catch (\Exception $ex) {
@ -113,28 +117,19 @@ class UserController extends BaseController
* @Request({
* "email": "foo@example.com",
* "password": "foopassword",
* "admin": false
* "admin": false,
* "custom_id": 123
* }, headers={"Authorization": "Bearer <token>"}),
* @Response(201),
* @Response(422, body={
* "message": "A validation error occured.",
* "errors": {
* "email": {"The email field is required."},
* "password": {"The password field is required."},
* "admin": {"The admin field is required."}
* },
* "status_code": 422
* })
* @Response(422)
* })
*/
public function create(Request $request)
{
try {
$user = new UserRepository;
$create = $user->create($request->input('email'), $request->input('password'), $request->input('admin'));
return $this->response->created(route('api.users.view', [
'id' => $create
]));
$create = $user->create($request->input('email'), $request->input('password'), $request->input('admin'), $request->input('custom_id'));
return [ 'id' => $create ];
} catch (DisplayValidationException $ex) {
throw new ResourceException('A validation error occured.', json_decode($ex->getMessage(), true));
} catch (DisplayException $ex) {

View file

@ -62,7 +62,7 @@ class DatabaseController extends Controller
'database_servers.*',
'nodes.name as a_linkedNode',
DB::raw('(SELECT COUNT(*) FROM `databases` WHERE `databases`.`db_server` = database_servers.id) as c_databases')
)->join('nodes', 'nodes.id', '=', 'database_servers.linked_node')
)->leftJoin('nodes', 'nodes.id', '=', 'database_servers.linked_node')
->paginate(20)
]);
}

View file

@ -27,6 +27,7 @@ use Alert;
use Debugbar;
use Log;
use DB;
use Validator;
use Pterodactyl\Models;
use Pterodactyl\Repositories\NodeRepository;
@ -65,6 +66,11 @@ class NodesController extends Controller
public function getNew(Request $request)
{
if (!Models\Location::all()->count()) {
Alert::warning('You must add a location before you can add a new node.')->flash();
return redirect()->route('admin.locations');
}
return view('admin.nodes.new', [
'locations' => Models\Location::all()
]);
@ -77,9 +83,10 @@ class NodesController extends Controller
$new = $node->create($request->except([
'_token'
]));
Alert::success('Successfully created new node. You should allocate some IP addresses to it now.')->flash();
Alert::success('Successfully created new node. <strong>Before you can add any servers you need to first assign some IP addresses and ports.</strong>')->flash();
return redirect()->route('admin.nodes.view', [
'id' => $new
'id' => $new,
'tab' => 'tab_allocation'
]);
} catch (DisplayValidationException $e) {
return redirect()->route('admin.nodes.new')->withErrors(json_decode($e->getMessage()))->withInput();
@ -95,32 +102,25 @@ class NodesController extends Controller
public function getView(Request $request, $id)
{
$node = Models\Node::findOrFail($id);
$allocations = [];
$alloc = Models\Allocation::select('ip', 'port', 'assigned_to')->where('node', $node->id)->orderBy('ip', 'asc')->orderBy('port', 'asc')->get();
if ($alloc) {
foreach($alloc as &$alloc) {
if (!array_key_exists($alloc->ip, $allocations)) {
$allocations[$alloc->ip] = [[
'port' => $alloc->port,
'assigned_to' => $alloc->assigned_to
]];
} else {
array_push($allocations[$alloc->ip], [
'port' => $alloc->port,
'assigned_to' => $alloc->assigned_to
]);
}
}
}
return view('admin.nodes.view', [
'node' => $node,
'servers' => Models\Server::select('servers.*', 'users.email as a_ownerEmail', 'services.name as a_serviceName')
->join('users', 'users.id', '=', 'servers.owner')
->join('services', 'services.id', '=', 'servers.service')
->where('node', $id)->paginate(10),
->where('node', $id)->paginate(10, ['*'], 'servers'),
'stats' => Models\Server::select(DB::raw('SUM(memory) as memory, SUM(disk) as disk'))->where('node', $node->id)->first(),
'locations' => Models\Location::all(),
'allocations' => json_decode(json_encode($allocations), false),
'allocations' => Models\Allocation::select('allocations.*', 'servers.name as assigned_to_name')
->where('allocations.node', $node->id)
->leftJoin('servers', 'servers.id', '=', 'allocations.assigned_to')
->orderBy('allocations.ip', 'asc')
->orderBy('allocations.port', 'asc')
->paginate(20, ['*'], 'allocations'),
'allocation_ips' => Models\Allocation::select('id', 'ip')
->where('node', $node->id)
->groupBy('ip')
->get(),
]);
}
@ -150,24 +150,51 @@ class NodesController extends Controller
])->withInput();
}
public function deleteAllocation(Request $request, $id, $ip, $port = null)
public function deallocateSingle(Request $request, $node, $allocation)
{
$query = Models\Allocation::where('node', $id)->whereNull('assigned_to')->where('ip', $ip);
if (is_null($port) || $port === 'undefined') {
$allocation = $query;
} else {
$allocation = $query->where('port', $port)->first();
}
if (!$allocation) {
$query = Models\Allocation::where('node', $node)->whereNull('assigned_to')->where('id', $allocation)->delete();
if ((int) $query === 0) {
return response()->json([
'error' => 'Unable to find an allocation matching those details to delete.'
], 400);
}
$allocation->delete();
return response('', 204);
}
public function deallocateBlock(Request $request, $node)
{
$query = Models\Allocation::where('node', $node)->whereNull('assigned_to')->where('ip', $request->input('ip'))->delete();
if ((int) $query === 0) {
Alert::danger('There was an error while attempting to delete allocations on that IP.')->flash();
return redirect()->route('admin.nodes.view', [
'id' => $node,
'tab' => 'tab_allocations'
]);
}
Alert::success('Deleted all unallocated ports for <code>' . $request->input('ip') . '</code>.')->flash();
return redirect()->route('admin.nodes.view', [
'id' => $node,
'tab' => 'tab_allocation'
]);
}
public function setAlias(Request $request, $node)
{
if (!$request->input('allocation')) {
return response('Missing required parameters.', 422);
}
try {
$update = Models\Allocation::findOrFail($request->input('allocation'));
$update->ip_alias = (empty($request->input('alias'))) ? null : $request->input('alias');
$update->save();
return response('', 204);
} catch (\Exception $ex) {
throw $ex;
}
}
public function getAllocationsJson(Request $request, $id)
{
$allocations = Models\Allocation::select('ip')->where('node', $id)->groupBy('ip')->get();
@ -176,6 +203,19 @@ class NodesController extends Controller
public function postAllocations(Request $request, $id)
{
$validator = Validator::make($request->all(), [
'allocate_ip.*' => 'required|string',
'allocate_port.*' => 'required'
]);
if ($validator->fails()) {
return redirect()->route('admin.nodes.view', [
'id' => $id,
'tab' => 'tab_allocation'
])->withErrors($validator->errors())->withInput();
}
$processedData = [];
foreach($request->input('allocate_ip') as $ip) {
if (!array_key_exists($ip, $processedData)) {
@ -195,9 +235,6 @@ class NodesController extends Controller
}
try {
if(empty($processedData)) {
throw new DisplayException('It seems that no data was passed to this function.');
}
$node = new NodeRepository;
$node->addAllocations($id, $processedData);
Alert::success('Successfully added new allocations to this node.')->flash();

View file

@ -51,8 +51,40 @@ class ServersController extends Controller
public function getIndex(Request $request)
{
return view('admin.servers.index', [
'servers' => Models\Server::select(
$query = Models\Server::withTrashed()->select(
'servers.*',
'nodes.name as a_nodeName',
'users.email as a_ownerEmail',
'allocations.ip',
'allocations.port',
'allocations.ip_alias'
)->join('nodes', 'servers.node', '=', 'nodes.id')
->join('users', 'servers.owner', '=', 'users.id')
->join('allocations', 'servers.allocation', '=', 'allocations.id');
if ($request->input('filter') && !is_null($request->input('filter'))) {
preg_match_all('/[^\s"\']+|"([^"]*)"|\'([^\']*)\'/', urldecode($request->input('filter')), $matches);
foreach($matches[0] as $match) {
$match = str_replace('"', '', $match);
if (strpos($match, ':')) {
list($field, $term) = explode(':', $match);
$field = (strpos($field, '.')) ? $field : 'servers.' . $field;
$query->orWhere($field, 'LIKE', '%' . $term . '%');
} else {
$query->where('servers.name', 'LIKE', '%' . $match . '%');
$query->orWhere('servers.username', 'LIKE', '%' . $match . '%');
$query->orWhere('users.email', 'LIKE', '%' . $match . '%');
$query->orWhere('allocations.port', 'LIKE', '%' . $match . '%');
$query->orWhere('allocations.ip', 'LIKE', '%' . $match . '%');
}
}
}
try {
$servers = $query->paginate(20);
} catch (\Exception $ex) {
Alert::warning('There was an error with the search parameters provided.');
$servers = Models\Server::withTrashed()->select(
'servers.*',
'nodes.name as a_nodeName',
'users.email as a_ownerEmail',
@ -62,7 +94,11 @@ class ServersController extends Controller
)->join('nodes', 'servers.node', '=', 'nodes.id')
->join('users', 'servers.owner', '=', 'users.id')
->join('allocations', 'servers.allocation', '=', 'allocations.id')
->paginate(20),
->paginate(20);
}
return view('admin.servers.index', [
'servers' => $servers
]);
}
@ -76,7 +112,7 @@ class ServersController extends Controller
public function getView(Request $request, $id)
{
$server = Models\Server::select(
$server = Models\Server::withTrashed()->select(
'servers.*',
'nodes.name as a_nodeName',
'users.email as a_ownerEmail',
@ -358,7 +394,7 @@ class ServersController extends Controller
try {
$server = new ServerRepository;
$server->deleteServer($id, $force);
Alert::success('Server was successfully deleted from the panel and the daemon.')->flash();
Alert::success('Server has been marked for deletion on the system.')->flash();
return redirect()->route('admin.servers');
} catch (DisplayException $ex) {
Alert::danger($ex->getMessage())->flash();
@ -474,4 +510,31 @@ class ServersController extends Controller
}
}
public function postQueuedDeletionHandler(Request $request, $id)
{
try {
$repo = new ServerRepository;
if (!is_null($request->input('cancel'))) {
$repo->cancelDeletion($id);
Alert::success('Server deletion has been cancelled. This server will remain suspended until you unsuspend it.')->flash();
return redirect()->route('admin.servers.view', $id);
} else if(!is_null($request->input('delete'))) {
$repo->deleteNow($id);
Alert::success('Server was successfully deleted from the system.')->flash();
return redirect()->route('admin.servers');
} else if(!is_null($request->input('force_delete'))) {
$repo->deleteNow($id, true);
Alert::success('Server was successfully force deleted from the system.')->flash();
return redirect()->route('admin.servers');
}
} catch (DisplayException $ex) {
Alert::danger($ex->getMessage())->flash();
return redirect()->route('admin.servers.view', $id);
} catch (\Exception $ex) {
Log::error($ex);
Alert::danger('An unhandled error occured while attempting to perform this action.')->flash();
return redirect()->route('admin.servers.view', $id);
}
}
}

View file

@ -130,4 +130,12 @@ class UserController extends Controller
return redirect()->route('admin.users.view', $user);
}
public function getJson(Request $request)
{
foreach(User::select('email')->get() as $user) {
$resp[] = $user->email;
}
return $resp;
}
}

View file

@ -2,6 +2,7 @@
namespace Pterodactyl\Http\Controllers\Auth;
use Pterodactyl\Models\User;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
@ -31,4 +32,12 @@ class ResetPasswordController extends Controller
{
$this->middleware('guest');
}
protected function rules() {
return [
'token' => 'required', 'email' => 'required|email',
'password' => 'required|confirmed|' . User::PASSWORD_RULES,
];
}
}

View file

@ -2,6 +2,7 @@
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com>
* Some Modifications (c) 2015 Dylan Seidt <dylan.seidt@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@ -21,74 +22,62 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Http\Controllers\Admin;
namespace Pterodactyl\Http\Controllers\Base;
use Alert;
use Log;
use Pterodactyl\Models;
use Pterodactyl\Repositories\APIRepository;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Repositories\APIRepository;
use Pterodactyl\Exceptions\DisplayValidationException;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Http\Request;
class APIController extends Controller
{
public function __construct()
public function index(Request $request)
{
//
}
public function getIndex(Request $request)
{
$keys = Models\APIKey::all();
$keys = Models\APIKey::where('user', $request->user()->id)->get();
foreach($keys as &$key) {
$key->permissions = Models\APIPermission::where('key_id', $key->id)->get();
}
return view('admin.api.index', [
return view('base.api.index', [
'keys' => $keys
]);
}
public function getNew(Request $request)
public function new(Request $request)
{
return view('admin.api.new');
return view('base.api.new');
}
public function postNew(Request $request)
public function save(Request $request)
{
try {
$api = new APIRepository;
$secret = $api->new($request->except(['_token']));
// Alert::info('An API Keypair has successfully been generated. The API secret for this public key is shown below and will not be shown again.<br /><br />Secret: <code>' . $secret . '</code>')->flash();
Alert::info("<script type='text/javascript'>swal({
type: 'info',
title: 'Secret Key',
html: true,
text: 'The secret for this keypair is shown below and will not be shown again.<hr /><code style=\'text-align:center;\'>" . $secret . "</code>'
});</script>")->flash();
return redirect()->route('admin.api');
$repo = new APIRepository($request->user());
$secret = $repo->new($request->except(['_token']));
Alert::success('An API Keypair has successfully been generated. The API secret for this public key is shown below and will not be shown again.<br /><br /><code>' . $secret . '</code>')->flash();
return redirect()->route('account.api');
} catch (DisplayValidationException $ex) {
return redirect()->route('admin.api.new')->withErrors(json_decode($ex->getMessage()))->withInput();
return redirect()->route('account.api.new')->withErrors(json_decode($ex->getMessage()))->withInput();
} catch (DisplayException $ex) {
Alert::danger($ex->getMessage())->flash();
} catch (\Exception $ex) {
Log::error($ex);
Alert::danger('An unhandled exception occured while attempting to add this API key.')->flash();
}
return redirect()->route('admin.api.new')->withInput();
return redirect()->route('account.api.new')->withInput();
}
public function deleteRevokeKey(Request $request, $key)
public function revoke(Request $request, $key)
{
try {
$api = new APIRepository;
$api->revoke($key);
$repo = new APIRepository($request->user());
$repo->revoke($key);
return response('', 204);
} catch (\Exception $ex) {
return response()->json([
@ -96,5 +85,4 @@ class APIController extends Controller
], 503);
}
}
}

View file

@ -0,0 +1,109 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com>
* Some Modifications (c) 2015 Dylan Seidt <dylan.seidt@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Http\Controllers\Base;
use Alert;
use Pterodactyl\Models\User;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Http\Request;
class AccountController extends Controller
{
/**
* Display base account information page.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request)
{
return view('base.account');
}
/**
* Update an account email.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function email(Request $request)
{
$this->validate($request, [
'new_email' => 'required|email',
'password' => 'required'
]);
$user = $request->user();
if (!password_verify($request->input('password'), $user->password)) {
Alert::danger('The password provided was not valid for this account.')->flash();
return redirect()->route('account');
}
$user->email = $request->input('new_email');
$user->save();
Alert::success('Your email address has successfully been updated.')->flash();
return redirect()->route('account');
}
/**
* Update an account password.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function password(Request $request)
{
$this->validate($request, [
'current_password' => 'required',
'new_password' => 'required|confirmed|different:current_password|' . User::PASSWORD_RULES,
'new_password_confirmation' => 'required'
]);
$user = $request->user();
if (!password_verify($request->input('current_password'), $user->password)) {
Alert::danger('The password provided was not valid for this account.')->flash();
return redirect()->route('account');
}
try {
$user->setPassword($request->input('new_password'));
Alert::success('Your password has successfully been updated.')->flash();
} catch (DisplayException $e) {
Alert::danger($e->getMessage())->flash();
}
return redirect()->route('account');
}
}

View file

@ -24,15 +24,9 @@
*/
namespace Pterodactyl\Http\Controllers\Base;
use Auth;
use Hash;
use Google2FA;
use Alert;
use Pterodactyl\Models;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Models\Server;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Http\Request;
class IndexController extends Controller
@ -55,7 +49,7 @@ class IndexController extends Controller
public function getIndex(Request $request)
{
return view('base.index', [
'servers' => Models\Server::getUserServers(10),
'servers' => Server::getUserServers(10),
]);
}
@ -71,169 +65,4 @@ class IndexController extends Controller
return str_random($length);
}
/**
* Returns Security Management Page.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function getAccountSecurity(Request $request)
{
return view('base.security', [
'sessions' => Models\Session::where('user_id', Auth::user()->id)->get()
]);
}
/**
* Generates TOTP Secret and returns popup data for user to verify
* that they can generate a valid response.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function putAccountTotp(Request $request)
{
$user = $request->user();
$user->totp_secret = Google2FA::generateSecretKey();
$user->save();
return response()->json([
'qrImage' => Google2FA::getQRCodeGoogleUrl(
'Pterodactyl',
$user->email,
$user->totp_secret
),
'secret' => $user->totp_secret
]);
}
/**
* Verifies that 2FA token recieved is valid and will work on the account.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function postAccountTotp(Request $request)
{
if (!$request->has('token')) {
return response(null, 500);
}
$user = $request->user();
if($user->toggleTotp($request->input('token'))) {
return response('true');
}
return response('false');
}
/**
* Disables TOTP on an account.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function deleteAccountTotp(Request $request)
{
if (!$request->has('token')) {
Alert::danger('Missing required `token` field in request.')->flash();
return redirect()->route('account.totp');
}
$user = $request->user();
if($user->toggleTotp($request->input('token'))) {
return redirect()->route('account.totp');
}
Alert::danger('The TOTP token provided was invalid.')->flash();
return redirect()->route('account.totp');
}
/**
* Display base account information page.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function getAccount(Request $request)
{
return view('base.account');
}
/**
* Update an account email.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function postAccountEmail(Request $request)
{
$this->validate($request, [
'new_email' => 'required|email',
'password' => 'required'
]);
$user = $request->user();
if (!password_verify($request->input('password'), $user->password)) {
Alert::danger('The password provided was not valid for this account.')->flash();
return redirect()->route('account');
}
$user->email = $request->input('new_email');
$user->save();
Alert::success('Your email address has successfully been updated.')->flash();
return redirect()->route('account');
}
/**
* Update an account password.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function postAccountPassword(Request $request)
{
$this->validate($request, [
'current_password' => 'required',
'new_password' => 'required|confirmed|different:current_password|regex:((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})',
'new_password_confirmation' => 'required'
]);
$user = $request->user();
if (!password_verify($request->input('current_password'), $user->password)) {
Alert::danger('The password provided was not valid for this account.')->flash();
return redirect()->route('account');
}
try {
$user->setPassword($request->input('new_password'));
Alert::success('Your password has successfully been updated.')->flash();
} catch (DisplayException $e) {
Alert::danger($e->getMessage())->flash();
}
return redirect()->route('account');
}
public function getRevokeSession(Request $request, $id)
{
$session = Models\Session::where('id', $id)->where('user_id', Auth::user()->id)->firstOrFail();
$session->delete();
return redirect()->route('account.security');
}
}

View file

@ -0,0 +1,130 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com>
* Some Modifications (c) 2015 Dylan Seidt <dylan.seidt@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Http\Controllers\Base;
use Google2FA;
use Alert;
use Pterodactyl\Models\Session;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Http\Request;
class SecurityController extends Controller
{
/**
* Returns Security Management Page.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request)
{
return view('base.security', [
'sessions' => Session::where('user_id', $request->user()->id)->get()
]);
}
/**
* Generates TOTP Secret and returns popup data for user to verify
* that they can generate a valid response.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function generateTotp(Request $request)
{
$user = $request->user();
$user->totp_secret = Google2FA::generateSecretKey();
$user->save();
return response()->json([
'qrImage' => Google2FA::getQRCodeGoogleUrl(
'Pterodactyl',
$user->email,
$user->totp_secret
),
'secret' => $user->totp_secret
]);
}
/**
* Verifies that 2FA token recieved is valid and will work on the account.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function setTotp(Request $request)
{
if (!$request->has('token')) {
return response(null, 500);
}
$user = $request->user();
if($user->toggleTotp($request->input('token'))) {
return response('true');
}
return response('false');
}
/**
* Disables TOTP on an account.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function disableTotp(Request $request)
{
if (!$request->has('token')) {
Alert::danger('Missing required `token` field in request.')->flash();
return redirect()->route('account.totp');
}
$user = $request->user();
if($user->toggleTotp($request->input('token'))) {
return redirect()->route('account.security');
}
Alert::danger('The TOTP token provided was invalid.')->flash();
return redirect()->route('account.security');
}
public function revoke(Request $request, $id)
{
$session = Session::where('id', $id)->where('user_id', $request->user()->id)->firstOrFail();
$session->delete();
return redirect()->route('account.security');
}
}

View file

@ -25,6 +25,7 @@ namespace Pterodactyl\Http\Controllers\Remote;
use Pterodactyl\Models;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Services\NotificationService;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Http\Request;
@ -82,4 +83,29 @@ class RemoteController extends Controller
], 200);
}
public function event(Request $request)
{
$server = Models\Server::where('uuid', $request->input('server'))->first();
if (!$server) {
return response()->json([
'error' => 'No server by that ID was found on the system.'
], 422);
}
$node = Models\Node::findOrFail($server->node);
$hmac = $request->input('signed');
if (base64_decode($hmac) !== hash_hmac('sha256', $server->uuid, $node->daemonSecret, true)) {
return response()->json([
'error' => 'Signed HMAC was invalid.'
], 403);
}
// Passes Validation, Setup Notifications
$notify = new NotificationService($server);
$notify->pass($request->input('notification'));
return response('', 201);
}
}

View file

@ -115,11 +115,12 @@ class AjaxController extends Controller
// Determine if we should show back links in the file browser.
// This code is strange, and could probably be rewritten much better.
$goBack = explode('/', rtrim($this->directory, '/'));
if (isset($goBack[2]) && !empty($goBack[2])) {
$goBack = explode('/', trim($this->directory, '/'));
if (!empty(array_filter($goBack)) && count($goBack) >= 2) {
$prevDir['show'] = true;
$prevDir['link'] = '/' . trim(str_replace(end($goBack), '', $this->directory), '/');
$prevDir['link_show'] = trim($prevDir['link'], '/');
array_pop($goBack);
$prevDir['link'] = '/' . implode('/', $goBack);
$prevDir['link_show'] = implode('/', $goBack) . '/';
}
$controller = new Repositories\Daemon\FileRepository($uuid);
@ -137,7 +138,7 @@ class AjaxController extends Controller
'server' => $server,
'files' => $directoryContents->files,
'folders' => $directoryContents->folders,
'extensions' => Repositories\HelperRepository::editableFiles(),
'editableMime' => Repositories\HelperRepository::editableFiles(),
'directory' => $prevDir
]);
@ -171,35 +172,40 @@ class AjaxController extends Controller
}
/**
* [postSetConnection description]
* [postSetPrimary description]
* @param Request $request
* @param string $uuid
* @return \Illuminate\Http\Response
*/
public function postSetConnection(Request $request, $uuid)
public function postSetPrimary(Request $request, $uuid)
{
$server = Models\Server::getByUUID($uuid);
$allocation = Models\Allocation::findOrFail($server->allocation);
$this->authorize('set-connection', $server);
if ($request->input('connection') === $allocation->ip . ':' . $allocation->port) {
if ((int) $request->input('allocation') === $server->allocation) {
return response()->json([
'error' => 'You are already using this as your default connection.'
], 409);
}
try {
$allocation = Models\Allocation::where('id', $request->input('allocation'))->where('assigned_to', $server->id)->first();
if (!$allocation) {
return response()->json([
'error' => 'No allocation matching your request was found in the system.'
], 422);
}
$repo = new Repositories\ServerRepository;
$repo->changeBuild($server->id, [
'default' => $request->input('connection'),
'default' => $allocation->ip . ':' . $allocation->port,
]);
return response('The default connection for this server has been updated. Please be aware that you will need to restart your server for this change to go into effect.');
} catch (DisplayValidationException $ex) {
return response()->json([
'error' => json_decode($ex->getMessage(), true),
], 503);
], 422);
} catch (DisplayException $ex) {
return response()->json([
'error' => $ex->getMessage(),

View file

@ -38,6 +38,8 @@ use Pterodactyl\Repositories\ServerRepository;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Http\Request;
use InvalidArgumentException;
class ServerController extends Controller
{
@ -51,13 +53,22 @@ class ServerController extends Controller
//
}
public function getJavascript(Request $request, $uuid, $file)
public function getJavascript(Request $request, $uuid, $folder, $file)
{
$server = Models\Server::getByUUID($uuid);
return response()->view('server.js.' . $server->a_serviceFile . '.' . basename($file, '.js'), [
$info = pathinfo($file);
$routeFile = str_replace('/', '.', $info['dirname']) . '.' . $info['filename'];
try {
return response()->view('server.js.' . $folder . '.' . $routeFile, [
'server' => $server,
'node' => Models\Node::find($server->node)
])->header('Content-Type', 'application/javascript');
} catch (InvalidArgumentException $ex) {
return abort(404);
} catch (\Exception $ex) {
throw $ex;
}
}
/**
@ -145,9 +156,9 @@ class ServerController extends Controller
'server' => $server,
'node' => Models\Node::find($server->node),
'file' => $file,
'contents' => $fileContent->content,
'directory' => (in_array($fileInfo->dirname, ['.', './', '/'])) ? '/' : trim($fileInfo->dirname, '/') . '/',
'extension' => $fileInfo->extension
'stat' => $fileContent['stat'],
'contents' => $fileContent['file']->content,
'directory' => (in_array($fileInfo->dirname, ['.', './', '/'])) ? '/' : trim($fileInfo->dirname, '/') . '/'
]);
}
@ -172,11 +183,11 @@ class ServerController extends Controller
$download->token = (string) Uuid::generate(4);
$download->server = $server->uuid;
$download->path = str_replace('../', '', $file);
$download->path = $file;
$download->save();
return redirect( $node->scheme . '://' . $node->fqdn . ':' . $node->daemonListen . '/server/download/' . $download->token);
return redirect( $node->scheme . '://' . $node->fqdn . ':' . $node->daemonListen . '/server/file/download/' . $download->token);
}

View file

@ -23,12 +23,16 @@
*/
namespace Pterodactyl\Http\Middleware;
use Auth;
use Crypt;
use Config;
use IPTools\IP;
use IPTools\Range;
use Pterodactyl\Models\APIKey;
use Pterodactyl\Models\APIPermission;
use Pterodactyl\Models\User;
use Pterodactyl\Services\APILogService;
use Illuminate\Http\Request;
use Dingo\Api\Routing\Route;
@ -50,7 +54,7 @@ class APISecretToken extends Authorization
public function __construct()
{
//
Config::set('session.driver', 'array');
}
public function getAuthorizationMethod()
@ -61,13 +65,15 @@ class APISecretToken extends Authorization
public function authenticate(Request $request, Route $route)
{
if (!$request->bearerToken() || empty($request->bearerToken())) {
throw new UnauthorizedHttpException('The authentication header was missing or malformed');
APILogService::log($request, 'The authentication header was missing or malformed.');
throw new UnauthorizedHttpException('The authentication header was missing or malformed.');
}
list($public, $hashed) = explode('.', $request->bearerToken());
$key = APIKey::where('public', $public)->first();
if (!$key) {
APILogService::log($request, 'Invalid API Key.');
throw new AccessDeniedHttpException('Invalid API Key.');
}
@ -82,34 +88,42 @@ class APISecretToken extends Authorization
}
}
if (!$inRange) {
APILogService::log($request, 'This IP address <' . $request->ip() . '> does not have permission to use this API key.');
throw new AccessDeniedHttpException('This IP address <' . $request->ip() . '> does not have permission to use this API key.');
}
}
foreach(APIPermission::where('key_id', $key->id)->get() as &$row) {
if ($row->permission === '*' || $row->permission === $request->route()->getName()) {
$this->permissionAllowed = true;
continue;
}
$permission = APIPermission::where('key_id', $key->id)->where('permission', $request->route()->getName());
// Suport Wildcards
if (starts_with($request->route()->getName(), 'api.user')) {
$permission->orWhere('permission', 'api.user.*');
} else if(starts_with($request->route()->getName(), 'api.admin')) {
$permission->orWhere('permission', 'api.admin.*');
}
if (!$this->permissionAllowed) {
throw new AccessDeniedHttpException('You do not have permission to access this resource.');
if (!$permission->first()) {
APILogService::log($request, 'You do not have permission to access this resource. This API Key requires the ' . $request->route()->getName() . ' permission node.');
throw new AccessDeniedHttpException('You do not have permission to access this resource. This API Key requires the ' . $request->route()->getName() . ' permission node.');
}
}
try {
$decrypted = Crypt::decrypt($key->secret);
} catch (\Illuminate\Contracts\Encryption\DecryptException $ex) {
APILogService::log($request, 'There was an error while attempting to check your secret key.');
throw new HttpException('There was an error while attempting to check your secret key.');
}
$this->url = urldecode($request->fullUrl());
if($this->_generateHMAC($request->getContent(), $decrypted) !== base64_decode($hashed)) {
APILogService::log($request, 'The hashed body was not valid. Potential modification of contents in route.');
throw new BadRequestHttpException('The hashed body was not valid. Potential modification of contents in route.');
}
return true;
// Log the Route Access
APILogService::log($request, null, true);
return Auth::loginUsingId($key->user);
}

View file

@ -13,5 +13,6 @@ class VerifyCsrfToken extends BaseVerifier
*/
protected $except = [
'remote/*',
'api/*'
];
}

View file

@ -32,33 +32,50 @@ class APIRoutes
public function map(Router $router) {
$api = app('Dingo\Api\Routing\Router');
$api->version('v1', ['middleware' => 'api.auth'], function ($api) {
$api->version('v1', ['prefix' => 'api/me', 'middleware' => 'api.auth'], function ($api) {
$api->get('/', [
'as' => 'api.user.me',
'uses' => 'Pterodactyl\Http\Controllers\API\User\InfoController@me'
]);
$api->get('/server/{uuid}', [
'as' => 'api.user.server',
'uses' => 'Pterodactyl\Http\Controllers\API\User\ServerController@info'
]);
$api->put('/server/{uuid}', [
'as' => 'api.user.server.power',
'uses' => 'Pterodactyl\Http\Controllers\API\User\ServerController@power'
]);
});
$api->version('v1', ['prefix' => 'api', 'middleware' => 'api.auth'], function ($api) {
/**
* User Routes
*/
$api->get('users', [
'as' => 'api.users.list',
'as' => 'api.admin.users.list',
'uses' => 'Pterodactyl\Http\Controllers\API\UserController@list'
]);
$api->post('users', [
'as' => 'api.users.create',
'as' => 'api.admin.users.create',
'uses' => 'Pterodactyl\Http\Controllers\API\UserController@create'
]);
$api->get('users/{id}', [
'as' => 'api.users.view',
'as' => 'api.admin.users.view',
'uses' => 'Pterodactyl\Http\Controllers\API\UserController@view'
]);
$api->patch('users/{id}', [
'as' => 'api.users.update',
'as' => 'api.admin.users.update',
'uses' => 'Pterodactyl\Http\Controllers\API\UserController@update'
]);
$api->delete('users/{id}', [
'as' => 'api.users.delete',
'as' => 'api.admin.users.delete',
'uses' => 'Pterodactyl\Http\Controllers\API\UserController@delete'
]);
@ -66,42 +83,42 @@ class APIRoutes
* Server Routes
*/
$api->get('servers', [
'as' => 'api.servers.list',
'as' => 'api.admin.servers.list',
'uses' => 'Pterodactyl\Http\Controllers\API\ServerController@list'
]);
$api->post('servers', [
'as' => 'api.servers.create',
'as' => 'api.admin.servers.create',
'uses' => 'Pterodactyl\Http\Controllers\API\ServerController@create'
]);
$api->get('servers/{id}', [
'as' => 'api.servers.view',
'as' => 'api.admin.servers.view',
'uses' => 'Pterodactyl\Http\Controllers\API\ServerController@view'
]);
$api->patch('servers/{id}/config', [
'as' => 'api.servers.config',
'as' => 'api.admin.servers.config',
'uses' => 'Pterodactyl\Http\Controllers\API\ServerController@config'
]);
$api->patch('servers/{id}/build', [
'as' => 'api.servers.build',
'as' => 'api.admin.servers.build',
'uses' => 'Pterodactyl\Http\Controllers\API\ServerController@build'
]);
$api->post('servers/{id}/suspend', [
'as' => 'api.servers.suspend',
'as' => 'api.admin.servers.suspend',
'uses' => 'Pterodactyl\Http\Controllers\API\ServerController@suspend'
]);
$api->post('servers/{id}/unsuspend', [
'as' => 'api.servers.unsuspend',
'as' => 'api.admin.servers.unsuspend',
'uses' => 'Pterodactyl\Http\Controllers\API\ServerController@unsuspend'
]);
$api->delete('servers/{id}/{force?}', [
'as' => 'api.servers.delete',
'as' => 'api.admin.servers.delete',
'uses' => 'Pterodactyl\Http\Controllers\API\ServerController@delete'
]);
@ -109,27 +126,32 @@ class APIRoutes
* Node Routes
*/
$api->get('nodes', [
'as' => 'api.nodes.list',
'as' => 'api.admin.nodes.list',
'uses' => 'Pterodactyl\Http\Controllers\API\NodeController@list'
]);
$api->post('nodes', [
'as' => 'api.nodes.create',
'as' => 'api.admin.nodes.create',
'uses' => 'Pterodactyl\Http\Controllers\API\NodeController@create'
]);
$api->get('nodes/allocations', [
'as' => 'api.nodes.allocations',
'as' => 'api.admin.nodes.allocations',
'uses' => 'Pterodactyl\Http\Controllers\API\NodeController@allocations'
]);
$api->get('nodes/{id}', [
'as' => 'api.nodes.view',
'as' => 'api.admin.nodes.view',
'uses' => 'Pterodactyl\Http\Controllers\API\NodeController@view'
]);
$api->get('nodes/{id}/config', [
'as' => 'api.admin.nodes.view',
'uses' => 'Pterodactyl\Http\Controllers\API\NodeController@config'
]);
$api->delete('nodes/{id}', [
'as' => 'api.nodes.delete',
'as' => 'api.admin.nodes.delete',
'uses' => 'Pterodactyl\Http\Controllers\API\NodeController@delete'
]);
@ -137,7 +159,7 @@ class APIRoutes
* Location Routes
*/
$api->get('locations', [
'as' => 'api.locations.list',
'as' => 'api.admin.locations.list',
'uses' => 'Pterodactyl\Http\Controllers\API\LocationController@list'
]);
@ -145,12 +167,12 @@ class APIRoutes
* Service Routes
*/
$api->get('services', [
'as' => 'api.services.list',
'as' => 'api.admin.services.list',
'uses' => 'Pterodactyl\Http\Controllers\API\ServiceController@list'
]);
$api->get('services/{id}', [
'as' => 'api.services.view',
'as' => 'api.admin.services.view',
'uses' => 'Pterodactyl\Http\Controllers\API\ServiceController@view'
]);

View file

@ -73,6 +73,11 @@ class AdminRoutes {
'uses' => 'Admin\UserController@getIndex'
]);
$router->get('/accounts.json', [
'as' => 'admin.users.json',
'uses' => 'Admin\UserController@getJson'
]);
// View Specific Account
$router->get('/view/{id}', [
'as' => 'admin.users.view',
@ -205,6 +210,11 @@ class AdminRoutes {
'uses' => 'Admin\ServersController@deleteServer'
]);
$router->post('/view/{id}/queuedDeletion', [
'uses' => 'Admin\ServersController@postQueuedDeletionHandler',
'as' => 'admin.servers.post.queuedDeletion'
]);
});
// Node Routes
@ -243,8 +253,17 @@ class AdminRoutes {
'uses' => 'Admin\NodesController@postView'
]);
$router->delete('/view/{id}/allocation/{ip}/{port?}', [
'uses' => 'Admin\NodesController@deleteAllocation'
$router->delete('/view/{id}/deallocate/single/{allocation}', [
'uses' => 'Admin\NodesController@deallocateSingle'
]);
$router->post('/view/{id}/deallocate/block', [
'uses' => 'Admin\NodesController@deallocateBlock'
]);
$router->post('/view/{id}/alias', [
'as' => 'admin.nodes.alias',
'uses' => 'Admin\NodesController@setAlias'
]);
$router->get('/view/{id}/allocations.json', [
@ -294,32 +313,6 @@ class AdminRoutes {
]);
});
// API Routes
$router->group([
'prefix' => 'admin/api',
'middleware' => [
'auth',
'admin',
'csrf'
]
], function () use ($router) {
$router->get('/', [
'as' => 'admin.api',
'uses' => 'Admin\APIController@getIndex'
]);
$router->get('/new', [
'as' => 'admin.api.new',
'uses' => 'Admin\APIController@getNew'
]);
$router->post('/new', [
'uses' => 'Admin\APIController@postNew'
]);
$router->delete('/revoke/{key?}', [
'as' => 'admin.api.revoke',
'uses' => 'Admin\APIController@deleteRevokeKey'
]);
});
// Database Routes
$router->group([
'prefix' => 'admin/databases',

View file

@ -51,21 +51,46 @@ class BaseRoutes {
// Account Routes
$router->group([
'profix' => 'account',
'prefix' => 'account',
'middleware' => [
'auth',
'csrf'
]
], function () use ($router) {
$router->get('account', [
$router->get('/', [
'as' => 'account',
'uses' => 'Base\IndexController@getAccount'
'uses' => 'Base\AccountController@index'
]);
$router->post('/account/password', [
'uses' => 'Base\IndexController@postAccountPassword'
$router->post('/password', [
'uses' => 'Base\AccountController@password'
]);
$router->post('/account/email', [
'uses' => 'Base\IndexController@postAccountEmail'
$router->post('/email', [
'uses' => 'Base\AccountController@email'
]);
});
// API Management Routes
$router->group([
'prefix' => 'account/api',
'middleware' => [
'auth',
'csrf'
]
], function () use ($router) {
$router->get('/', [
'as' => 'account.api',
'uses' => 'Base\APIController@index'
]);
$router->get('/new', [
'as' => 'account.api.new',
'uses' => 'Base\APIController@new'
]);
$router->post('/new', [
'uses' => 'Base\APIController@save'
]);
$router->delete('/revoke/{key}', [
'uses' => 'Base\APIController@revoke'
]);
});
@ -79,20 +104,20 @@ class BaseRoutes {
], function () use ($router) {
$router->get('/', [
'as' => 'account.security',
'uses' => 'Base\IndexController@getAccountSecurity'
'uses' => 'Base\SecurityController@index'
]);
$router->get('/revoke/{id}', [
'as' => 'account.security.revoke',
'uses' => 'Base\IndexController@getRevokeSession'
'uses' => 'Base\SecurityController@revoke'
]);
$router->put('/', [
'uses' => 'Base\IndexController@putAccountTotp'
$router->put('/totp', [
'uses' => 'Base\SecurityController@generateTotp'
]);
$router->post('/', [
'uses' => 'Base\IndexController@postAccountTotp'
$router->post('/totp', [
'uses' => 'Base\SecurityController@setTotp'
]);
$router->delete('/', [
'uses' => 'Base\IndexController@deleteAccountTotp'
$router->delete('/totp', [
'uses' => 'Base\SecurityController@disableTotp'
]);
});

View file

@ -40,6 +40,11 @@ class RemoteRoutes {
'as' => 'remote.install',
'uses' => 'Remote\RemoteController@postInstall'
]);
$router->post('event', [
'as' => 'remote.event',
'uses' => 'Remote\RemoteController@event'
]);
});
}

View file

@ -28,6 +28,7 @@ use Illuminate\Routing\Router;
class ServerRoutes {
public function map(Router $router) {
$router->group([
'prefix' => 'server/{server}',
'middleware' => [
@ -36,6 +37,7 @@ class ServerRoutes {
'csrf'
]
], function ($server) use ($router) {
// Index View for Server
$router->get('/', [
'as' => 'server.index',
@ -154,8 +156,8 @@ class ServerRoutes {
]);
// Sets the Default Connection for the Server
$router->post('set-connection', [
'uses' => 'Server\AjaxController@postSetConnection'
$router->post('set-primary', [
'uses' => 'Server\AjaxController@postSetPrimary'
]);
$router->post('settings/reset-database-password', [
@ -167,7 +169,7 @@ class ServerRoutes {
// Assorted AJAX Routes
$router->group(['prefix' => 'js'], function ($server) use ($router) {
// Returns Server Status
$router->get('{file}', [
$router->get('{folder}/{file}', [
'as' => 'server.js',
'uses' => 'Server\ServerController@getJavascript'
])->where('file', '.*');

67
app/Jobs/DeleteServer.php Normal file
View file

@ -0,0 +1,67 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Jobs;
use DB;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Pterodactyl\Models;
use Pterodactyl\Repositories\ServerRepository;
class DeleteServer extends Job implements ShouldQueue
{
use InteractsWithQueue, SerializesModels;
/**
* Id of server to be deleted.
* @var object
*/
protected $id;
/**
* Create a new job instance.
*
* @param integer $server
* @return void
*/
public function __construct($id)
{
$this->id = $id;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$repo = new ServerRepository;
$repo->deleteNow($this->id);
}
}

View file

@ -0,0 +1,65 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Jobs;
use Debugbar;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Pterodactyl\Repositories\ServerRepository;
class SuspendServer extends Job implements ShouldQueue
{
use InteractsWithQueue, SerializesModels;
/**
* ID of associated server model.
* @var object
*/
protected $id;
/**
* Create a new job instance.
*
* @param integer $id
* @return void
*/
public function __construct($id)
{
$this->id = $id;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$repo = new ServerRepository;
$repo->suspend($this->id, true);
}
}

View file

@ -0,0 +1,64 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Listeners;
use Carbon;
use Pterodactyl\Events\ServerDeleted;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Pterodactyl\Jobs\SuspendServer;
use Pterodactyl\Jobs\DeleteServer;
class DeleteServerListener
{
use DispatchesJobs;
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param DeleteServerEvent $event
* @return void
*/
public function handle(ServerDeleted $event)
{
$this->dispatch((new SuspendServer($event->server))->onQueue(env('QUEUE_HIGH', 'high')));
$this->dispatch(
(new DeleteServer($event->server))
->delay(Carbon::now()->addMinutes(env('APP_DELETE_MINUTES', 10)))
->onQueue(env('QUEUE_STANDARD', 'standard'))
);
}
}

61
app/Models/APILog.php Normal file
View file

@ -0,0 +1,61 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Models;
use Illuminate\Database\Eloquent\Model;
class APILog extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'api_logs';
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = [];
/**
* Fields that are not mass assignable.
*
* @var array
*/
protected $guarded = ['id', 'created_at', 'updated_at'];
/**
* Cast values to correct type.
*
* @var array
*/
protected $casts = [
'authorized' => 'boolean'
];
}

View file

@ -24,13 +24,17 @@
namespace Pterodactyl\Models;
use Auth;
use Pterodactyl\Models\Subuser;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Pterodactyl\Exceptions\DisplayException;
class Server extends Model
{
use SoftDeletes;
/**
* The table associated with the model.
*
@ -43,17 +47,21 @@ class Server extends Model
*
* @var array
*/
protected $hidden = [
'daemonSecret',
'sftp_password'
];
protected $hidden = ['daemonSecret', 'sftp_password'];
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['deleted_at'];
/**
* Fields that are not mass assignable.
*
* @var array
*/
protected $guarded = ['id', 'installed', 'created_at', 'updated_at'];
protected $guarded = ['id', 'installed', 'created_at', 'updated_at', 'deleted_at'];
/**
* Cast values to correct type.
@ -91,6 +99,7 @@ class Server extends Model
*/
public function __construct()
{
parent::__construct();
self::$user = Auth::user();
}
@ -101,7 +110,7 @@ class Server extends Model
* @param Illuminate\Database\Eloquent\Model\Server $server
* @return string
*/
protected static function getUserDaemonSecret(Server $server)
public static function getUserDaemonSecret(Server $server)
{
if (self::$user->id === $server->owner || self::$user->root_admin === 1) {
@ -133,9 +142,13 @@ class Server extends Model
'locations.short as a_locationShort',
'allocations.ip',
'allocations.ip_alias',
'allocations.port'
'allocations.port',
'services.name as a_serviceName',
'service_options.name as a_serviceOptionName'
)->join('nodes', 'servers.node', '=', 'nodes.id')
->join('locations', 'nodes.location', '=', 'locations.id')
->join('services', 'servers.service', '=', 'services.id')
->join('service_options', 'servers.option', '=', 'service_options.id')
->join('allocations', 'servers.allocation', '=', 'allocations.id');
if (self::$user->root_admin !== 1) {
@ -167,7 +180,8 @@ class Server extends Model
$query = self::select('servers.*', 'services.file as a_serviceFile')
->join('services', 'services.id', '=', 'servers.service')
->where('uuidShort', $uuid);
->where('uuidShort', $uuid)
->orWhere('uuid', $uuid);
if (self::$user->root_admin !== 1) {
$query->whereIn('servers.id', Subuser::accessServers());

View file

@ -76,6 +76,13 @@ class User extends Model implements AuthenticatableContract,
*/
protected $hidden = ['password', 'remember_token', 'totp_secret'];
/**
* The rules for user passwords
*
* @var string
*/
const PASSWORD_RULES = 'min:8|regex:((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})';
public function permissions()
{
return $this->hasMany(Permission::class);

View file

@ -57,7 +57,7 @@ class AccountCreated extends Notification implements ShouldQueue
*/
public function via($notifiable)
{
return ['mail', 'database'];
return ['mail'];
}
/**
@ -74,17 +74,4 @@ class AccountCreated extends Notification implements ShouldQueue
->action('Setup Your Account', url('/auth/password/reset/' . $this->token . '?email=' . $notifiable->email));
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
'email' => $notifiable->email,
'token' => $this->token
];
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace Pterodactyl\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
class ServerCreated extends Notification implements ShouldQueue
{
use Queueable;
public $server;
/**
* Create a new notification instance.
*
* @param array $server
* @return void
*/
public function __construct(array $server)
{
$this->server = (object) $server;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->line('A new server as been assigned to your account.')
->line('Server Name: ' . $this->server->name)
->line('Memory: ' . $this->server->memory . ' MB')
->line('Node: ' . $this->server->node)
->line('Type: ' . $this->server->service . ' - ' . $this->server->option)
->action('Peel Off the Protective Wrap', route('server.index', $this->server->uuidShort))
->line('Please let us know if you have any additional questions or concerns!');
}
}

View file

@ -209,6 +209,70 @@ class ServerPolicy
return $user->permissions()->server($server)->permission('save-files')->exists();
}
/**
* Check if user has permission to move and rename files and folders on a server.
*
* @param Pterodactyl\Models\User $user
* @param Pterodactyl\Models\Server $server
* @return boolean
*/
public function moveFiles(User $user, Server $server)
{
if ($this->isOwner($user, $server)) {
return true;
}
return $user->permissions()->server($server)->permission('move-files')->exists();
}
/**
* Check if user has permission to copy folders and files on a server.
*
* @param Pterodactyl\Models\User $user
* @param Pterodactyl\Models\Server $server
* @return boolean
*/
public function copyFiles(User $user, Server $server)
{
if ($this->isOwner($user, $server)) {
return true;
}
return $user->permissions()->server($server)->permission('copy-files')->exists();
}
/**
* Check if user has permission to compress files and folders on a server.
*
* @param Pterodactyl\Models\User $user
* @param Pterodactyl\Models\Server $server
* @return boolean
*/
public function compressFiles(User $user, Server $server)
{
if ($this->isOwner($user, $server)) {
return true;
}
return $user->permissions()->server($server)->permission('compress-files')->exists();
}
/**
* Check if user has permission to decompress files on a server.
*
* @param Pterodactyl\Models\User $user
* @param Pterodactyl\Models\Server $server
* @return boolean
*/
public function decompressFiles(User $user, Server $server)
{
if ($this->isOwner($user, $server)) {
return true;
}
return $user->permissions()->server($server)->permission('decompress-files')->exists();
}
/**
* Check if user has permission to add files to a server.
*

View file

@ -13,8 +13,8 @@ class EventServiceProvider extends ServiceProvider
* @var array
*/
protected $listen = [
'Pterodactyl\Events\SomeEvent' => [
'Pterodactyl\Listeners\EventListener',
'Pterodactyl\Events\ServerDeleted' => [
'Pterodactyl\Listeners\DeleteServerListener',
],
];

View file

@ -23,6 +23,7 @@
*/
namespace Pterodactyl\Repositories;
use Auth;
use DB;
use Crypt;
use Validator;
@ -40,38 +41,51 @@ class APIRepository
* @var array
*/
protected $permissions = [
'admin' => [
'*',
// User Management Routes
'api.users.list',
'api.users.create',
'api.users.view',
'api.users.update',
'api.users.delete',
'users.list',
'users.create',
'users.view',
'users.update',
'users.delete',
// Server Manaement Routes
'api.servers.list',
'api.servers.create',
'api.servers.view',
'api.servers.config',
'api.servers.build',
'api.servers.suspend',
'api.servers.unsuspend',
'api.servers.delete',
'servers.list',
'servers.create',
'servers.view',
'servers.config',
'servers.build',
'servers.suspend',
'servers.unsuspend',
'servers.delete',
// Node Management Routes
'api.nodes.list',
'api.nodes.create',
'api.nodes.list',
'api.nodes.allocations',
'api.nodes.delete',
'nodes.list',
'nodes.create',
'nodes.list',
'nodes.allocations',
'nodes.delete',
// Service Routes
'api.services.list',
'api.services.view',
'services.list',
'services.view',
// Location Routes
'api.locations.list',
'locations.list',
],
'user' => [
'*',
// Informational
'me',
// Server Control
'server',
'server.power',
],
];
/**
@ -80,12 +94,17 @@ class APIRepository
*/
protected $allowed = [];
protected $user;
/**
* Constructor
*/
public function __construct()
public function __construct(Models\User $user = null)
{
//
$this->user = is_null($user) ? Auth::user() : $user;
if (is_null($this->user)) {
throw new \Exception('Cannot access API Repository without passing a user to __construct().');
}
}
/**
@ -101,7 +120,9 @@ class APIRepository
public function new(array $data)
{
$validator = Validator::make($data, [
'permissions' => 'required|array'
'memo' => 'string|max:500',
'permissions' => 'sometimes|required|array',
'adminPermissions' => 'sometimes|required|array'
]);
$validator->after(function($validator) use ($data) {
@ -125,31 +146,62 @@ class APIRepository
}
DB::beginTransaction();
try {
$secretKey = str_random(16) . '.' . str_random(15);
$secretKey = str_random(16) . '.' . str_random(7) . '.' . str_random(7);
$key = new Models\APIKey;
$key->fill([
'user' => $this->user->id,
'public' => str_random(16),
'secret' => Crypt::encrypt($secretKey),
'allowed_ips' => empty($this->allowed) ? null : json_encode($this->allowed)
'allowed_ips' => empty($this->allowed) ? null : json_encode($this->allowed),
'memo' => $data['memo'],
'expires_at' => null
]);
$key->save();
foreach($data['permissions'] as $permission) {
if (in_array($permission, $this->permissions)) {
$totalPermissions = 0;
if (isset($data['permissions'])) {
foreach($data['permissions'] as $permNode) {
if (!strpos($permNode, ':')) continue;
list($toss, $permission) = explode(':', $permNode);
if (in_array($permission, $this->permissions['user'])) {
$totalPermissions++;
$model = new Models\APIPermission;
$model->fill([
'key_id' => $key->id,
'permission' => $permission
'permission' => 'api.user.' . $permission
]);
$model->save();
}
}
}
if ($this->user->root_admin === 1 && isset($data['adminPermissions'])) {
foreach($data['adminPermissions'] as $permNode) {
if (!strpos($permNode, ':')) continue;
list($toss, $permission) = explode(':', $permNode);
if (in_array($permission, $this->permissions['admin'])) {
$totalPermissions++;
$model = new Models\APIPermission;
$model->fill([
'key_id' => $key->id,
'permission' => 'api.admin.' . $permission
]);
$model->save();
}
}
}
if ($totalPermissions < 1) {
throw new DisplayException('No valid permissions were passed.');
}
DB::commit();
return $secretKey;
} catch (\Exception $ex) {
DB::rollBack();
throw $ex;
}
@ -169,7 +221,7 @@ class APIRepository
DB::beginTransaction();
try {
$model = Models\APIKey::where('public', $key)->firstOrFail();
$model = Models\APIKey::where('public', $key)->where('user', $this->user->id)->firstOrFail();
$permissions = Models\APIPermission::where('key_id', $model->id)->delete();
$model->delete();

View file

@ -85,7 +85,7 @@ class FileRepository
* Get the contents of a requested file for the server.
*
* @param string $file
* @return string
* @return array
*/
public function returnFileContents($file)
{
@ -95,22 +95,39 @@ class FileRepository
}
$file = (object) pathinfo($file);
if (!in_array($file->extension, HelperRepository::editableFiles())) {
throw new DisplayException('You do not have permission to edit this type of file.');
}
$file->dirname = (in_array($file->dirname, ['.', './', '/'])) ? null : trim($file->dirname, '/') . '/';
$res = $this->client->request('GET', '/server/file/' . rawurlencode($file->dirname.$file->basename), [
$res = $this->client->request('GET', '/server/file/stat/' . rawurlencode($file->dirname.$file->basename) , [
'headers' => $this->headers
]);
$stat = json_decode($res->getBody());
if($res->getStatusCode() !== 200 || !isset($stat->size)) {
throw new DisplayException('The daemon provided a non-200 error code on stat lookup: HTTP\\' . $res->getStatusCode());
}
if (!in_array($stat->mime, HelperRepository::editableFiles())) {
throw new DisplayException('You cannot edit that type of file (' . $stat->mime . ') through the panel.');
}
if ($stat->size > 5000000) {
throw new DisplayException('That file is too large to open in the browser, consider using a SFTP client.');
}
$res = $this->client->request('GET', '/server/file/f/' . rawurlencode($file->dirname.$file->basename) , [
'headers' => $this->headers
]);
$json = json_decode($res->getBody());
if($res->getStatusCode() !== 200 || !isset($json->content)) {
throw new DisplayException('Scales provided a non-200 error code: HTTP\\' . $res->getStatusCode());
throw new DisplayException('The daemon provided a non-200 error code: HTTP\\' . $res->getStatusCode());
}
return $json;
return [
'file' => $json,
'stat' => $stat
];
}
@ -119,7 +136,7 @@ class FileRepository
*
* @param string $file
* @param string $content
* @return boolean
* @return bool
*/
public function saveFileContents($file, $content)
{
@ -130,15 +147,12 @@ class FileRepository
$file = (object) pathinfo($file);
if(!in_array($file->extension, HelperRepository::editableFiles())) {
throw new DisplayException('You do not have permission to edit this type of file.');
}
$file->dirname = (in_array($file->dirname, ['.', './', '/'])) ? null : trim($file->dirname, '/') . '/';
$res = $this->client->request('POST', '/server/file/' . rawurlencode($file->dirname.$file->basename), [
$res = $this->client->request('POST', '/server/file/save', [
'headers' => $this->headers,
'json' => [
'path' => rawurlencode($file->dirname.$file->basename),
'content' => $content
]
]);
@ -185,7 +199,8 @@ class FileRepository
'entry' => $value->name,
'directory' => trim($directory, '/'),
'size' => null,
'date' => strtotime($value->modified)
'date' => strtotime($value->modified),
'mime' => $value->mime
]]);
} else if ($value->file === true) {
@ -195,7 +210,8 @@ class FileRepository
'directory' => trim($directory, '/'),
'extension' => pathinfo($value->name, PATHINFO_EXTENSION),
'size' => HelperRepository::bytesToHuman($value->size),
'date' => strtotime($value->modified)
'date' => strtotime($value->modified),
'mime' => $value->mime
]]);
}

View file

@ -138,7 +138,7 @@ class DatabaseRepository {
$capsule->setAsGlobal();
Capsule::statement(sprintf(
'ALTER USER \'%s\'@\'%s\' IDENTIFIED BY \'%s\'',
'SET PASSWORD FOR \'%s\'@\'%s\' = PASSWORD(\'%s\')',
$db->username,
$db->remote,
$password

View file

@ -30,25 +30,20 @@ class HelperRepository {
* @var array
*/
protected static $editable = [
'txt',
'yml',
'yaml',
'log',
'conf',
'config',
'html',
'json',
'properties',
'props',
'cfg',
'lang',
'ini',
'cmd',
'sh',
'lua',
'0' // Supports BungeeCord Files
'application/json',
'application/javascript',
'application/xml',
'application/xhtml+xml',
'text/xml',
'text/css',
'text/html',
'text/plain',
'text/x-perl',
'text/x-shellscript',
'inode/x-empty'
];
public function __construct()
{
//

View file

@ -164,7 +164,17 @@ class NodeRepository {
try {
foreach($allocations as $rawIP => $ports) {
try {
$setAlias = null;
$parsedIP = Network::parse($rawIP);
} catch (\Exception $ex) {
try {
$setAlias = $rawIP;
$parsedIP = Network::parse(gethostbyname($rawIP));
} catch (\Exception $ex) {
throw $ex;
}
}
foreach($parsedIP as $ip) {
foreach($ports as $port) {
if (!is_int($port) && !preg_match('/^(\d{1,5})-(\d{1,5})$/', $port)) {
@ -182,6 +192,7 @@ class NodeRepository {
'node' => $node->id,
'ip' => $ip,
'port' => $assignPort,
'ip_alias' => $setAlias,
'assigned_to' => null
]);
$alloc->save();
@ -198,6 +209,7 @@ class NodeRepository {
'node' => $node->id,
'ip' => $ip,
'port' => $port,
'ip_alias' => $setAlias,
'assigned_to' => null
]);
$alloc->save();
@ -208,7 +220,7 @@ class NodeRepository {
}
DB::commit();
return true;
// return true;
} catch (\Exception $ex) {
DB::rollBack();
throw $ex;

View file

@ -31,6 +31,9 @@ use Log;
use Pterodactyl\Models;
use Pterodactyl\Services\UuidService;
use Pterodactyl\Services\DeploymentService;
use Pterodactyl\Notifications\ServerCreated;
use Pterodactyl\Events\ServerDeleted;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Exceptions\AccountNotFoundException;
@ -40,19 +43,7 @@ class ServerRepository
{
protected $daemonPermissions = [
's:get',
's:power:start',
's:power:stop',
's:power:restart',
's:power:kill',
's:console',
's:command',
's:files:get',
's:files:read',
's:files:post',
's:files:delete',
's:files:upload',
's:set-password'
's:*'
];
public function __construct()
@ -62,19 +53,17 @@ class ServerRepository
/**
* Generates a SFTP username for a server given a server name.
* format: mumble_67c7a4b0
*
* @param string $name
* @param string $uuid
* @return string
*/
protected function generateSFTPUsername($name)
protected function generateSFTPUsername($name, $uuid = null)
{
$name = preg_replace('/\s+/', '', $name);
if (strlen($name) > 6) {
return strtolower('ptdl-' . substr($name, 0, 6) . '_' . str_random(5));
}
return strtolower('ptdl-' . $name . '_' . str_random((11 - strlen($name))));
$uuid = is_null($uuid) ? str_random(8) : $uuid;
return strtolower(substr(preg_replace('/\s+/', '', $name), 0, 6) . '_' . $uuid);
}
@ -88,42 +77,75 @@ class ServerRepository
// Validate Fields
$validator = Validator::make($data, [
'owner' => 'required|email|exists:users,email',
'node' => 'required|numeric|min:1|exists:nodes,id',
'owner' => 'bail|required',
'name' => 'required|regex:/^([\w -]{4,35})$/',
'memory' => 'required|numeric|min:0',
'swap' => 'required|numeric|min:-1',
'io' => 'required|numeric|min:10|max:1000',
'cpu' => 'required|numeric|min:0',
'disk' => 'required|numeric|min:0',
'allocation' => 'numeric|exists:allocations,id|required_without:ip,port',
'ip' => 'required_without:allocation|ip',
'port' => 'required_without:allocation|numeric|min:1|max:65535',
'service' => 'required|numeric|min:1|exists:services,id',
'option' => 'required|numeric|min:1|exists:service_options,id',
'service' => 'bail|required|numeric|min:1|exists:services,id',
'option' => 'bail|required|numeric|min:1|exists:service_options,id',
'startup' => 'string',
'custom_image_name' => 'required_if:use_custom_image,on',
'auto_deploy' => 'sometimes|boolean'
]);
$validator->sometimes('node', 'bail|required|numeric|min:1|exists:nodes,id', function ($input) {
return !($input->auto_deploy);
});
$validator->sometimes('ip', 'required|ip', function ($input) {
return (!$input->auto_deploy && !$input->allocation);
});
$validator->sometimes('port', 'required|numeric|min:1|max:65535', function ($input) {
return (!$input->auto_deploy && !$input->allocation);
});
$validator->sometimes('allocation', 'numeric|exists:allocations,id', function ($input) {
return !($input->auto_deploy || ($input->port && $input->ip));
});
// Run validator, throw catchable and displayable exception if it fails.
// Exception includes a JSON result of failed validation rules.
if ($validator->fails()) {
throw new DisplayValidationException($validator->errors());
}
// Get the User ID; user exists since we passed the 'exists:users,email' part of the validation
$user = Models\User::select('id')->where('email', $data['owner'])->first();
if (is_int($data['owner'])) {
$user = Models\User::select('id', 'email')->where('id', $data['owner'])->first();
} else {
$user = Models\User::select('id', 'email')->where('email', $data['owner'])->first();
}
// Get Node Information
if (!$user) {
throw new DisplayException('The user id or email passed to the function was not found on the system.');
}
$autoDeployed = false;
if (isset($data['auto_deploy']) && in_array($data['auto_deploy'], [true, 1, "1"])) {
// This is an auto-deployment situation
// Ignore any other passed node data
unset($data['node'], $data['ip'], $data['port'], $data['allocation']);
$autoDeployed = true;
$node = DeploymentService::smartRandomNode($data['memory'], $data['disk'], $data['location']);
$allocation = DeploymentService::randomAllocation($node->id);
} else {
$node = Models\Node::getByID($data['node']);
}
// Verify IP & Port are a.) free and b.) assigned to the node.
// We know the node exists because of 'exists:nodes,id' in the validation
if (!$autoDeployed) {
if (!isset($data['allocation'])) {
$allocation = Models\Allocation::where('ip', $data['ip'])->where('port', $data['port'])->where('node', $data['node'])->whereNull('assigned_to')->first();
} else {
$allocation = Models\Allocation::where('id' , $data['allocation'])->where('node', $data['node'])->whereNull('assigned_to')->first();
}
}
// Something failed in the query, either that combo doesn't exist, or it is in use.
if (!$allocation) {
@ -176,6 +198,7 @@ class ServerRepository
}
// Check Overallocation
if (!$autoDeployed) {
if (is_numeric($node->memory_overallocate) || is_numeric($node->disk_overallocate)) {
$totals = Models\Server::select(DB::raw('SUM(memory) as memory, SUM(disk) as disk'))->where('node', $node->id)->first();
@ -197,7 +220,7 @@ class ServerRepository
throw new DisplayException('The amount of disk allocated to this server would put the node over its allocation limits. This node is allowed ' . ($node->disk_overallocate + 100) . '% of its assigned ' . $node->disk . 'Mb of disk (' . $diskLimit . 'Mb) of which ' . (($totals->disk / $node->disk) * 100) . '% (' . $totals->disk . 'Mb) is in use already. By allocating this server the node would be at ' . (($newDisk / $node->disk) * 100) . '% (' . $newDisk . 'Mb) usage.');
}
}
}
}
DB::beginTransaction();
@ -207,11 +230,12 @@ class ServerRepository
// Add Server to the Database
$server = new Models\Server;
$generatedUuid = $uuid->generate('servers', 'uuid');
$genUuid = $uuid->generate('servers', 'uuid');
$genShortUuid = $uuid->generateShort('servers', 'uuidShort', $genUuid);
$server->fill([
'uuid' => $generatedUuid,
'uuidShort' => $uuid->generateShort('servers', 'uuidShort', $generatedUuid),
'node' => $data['node'],
'uuid' => $genUuid,
'uuidShort' => $genShortUuid,
'node' => $node->id,
'name' => $data['name'],
'suspended' => 0,
'owner' => $user->id,
@ -226,7 +250,9 @@ class ServerRepository
'option' => $data['option'],
'startup' => $data['startup'],
'daemonSecret' => $uuid->generate('servers', 'daemonSecret'),
'username' => $this->generateSFTPUsername($data['name'])
'image' => (isset($data['custom_image_name'])) ? $data['custom_image_name'] : $option->docker_image,
'username' => $this->generateSFTPUsername($data['name'], $genShortUuid),
'sftp_password' => Crypt::encrypt('not set')
]);
$server->save();
@ -250,6 +276,16 @@ class ServerRepository
]);
}
// Queue Notification Email
$user->notify((new ServerCreated([
'name' => $server->name,
'memory' => $server->memory,
'node' => $node->name,
'service' => $service->name,
'option' => $option->name,
'uuidShort' => $server->uuidShort
])));
$client = Models\Node::guzzleRequest($node->id);
$client->request('POST', '/servers', [
'headers' => [
@ -292,7 +328,6 @@ class ServerRepository
throw new DisplayException('There was an error while attempting to connect to the daemon to add this server.', $ex);
} catch (\Exception $ex) {
DB::rollBack();
Log:error($ex);
throw $ex;
}
@ -498,19 +533,19 @@ class ServerRepository
$newPorts = false;
// Remove Assignments
if (isset($data['remove_additional'])) {
$newPorts = true;
foreach ($data['remove_additional'] as $id => $combo) {
list($ip, $port) = explode(':', $combo);
// Invalid, not worth killing the whole thing, we'll just skip over it.
if (!filter_var($ip, FILTER_VALIDATE_IP) || !preg_match('/^(\d{1,5})$/', $port)) {
continue;
break;
}
// Can't remove the assigned IP/Port combo
if ($ip === $allocation->ip && $port === $allocation->port) {
continue;
if ($ip === $allocation->ip && (int) $port === (int) $allocation->port) {
break;
}
$newPorts = true;
Models\Allocation::where('ip', $ip)->where('port', $port)->where('assigned_to', $server->id)->update([
'assigned_to' => null
]);
@ -519,19 +554,19 @@ class ServerRepository
// Add Assignments
if (isset($data['add_additional'])) {
$newPorts = true;
foreach ($data['add_additional'] as $id => $combo) {
list($ip, $port) = explode(':', $combo);
// Invalid, not worth killing the whole thing, we'll just skip over it.
if (!filter_var($ip, FILTER_VALIDATE_IP) || !preg_match('/^(\d{1,5})$/', $port)) {
continue;
break;
}
// Don't allow double port assignments
if (Models\Allocation::where('port', $port)->where('assigned_to', $server->id)->count() !== 0) {
continue;
break;
}
$newPorts = true;
Models\Allocation::where('ip', $ip)->where('port', $port)->whereNull('assigned_to')->update([
'assigned_to' => $server->id
]);
@ -555,38 +590,38 @@ class ServerRepository
// @TODO: verify that server can be set to this much memory without
// going over node limits.
if (isset($data['memory'])) {
if (isset($data['memory']) && $server->memory !== (int) $data['memory']) {
$server->memory = $data['memory'];
$newBuild['memory'] = (int) $server->memory;
}
if (isset($data['swap'])) {
if (isset($data['swap']) && $server->swap !== (int) $data['swap']) {
$server->swap = $data['swap'];
$newBuild['swap'] = (int) $server->swap;
}
// @TODO: verify that server can be set to this much disk without
// going over node limits.
if (isset($data['disk'])) {
if (isset($data['disk']) && $server->disk !== (int) $data['disk']) {
$server->disk = $data['disk'];
$newBuild['disk'] = (int) $server->disk;
}
if (isset($data['cpu'])) {
if (isset($data['cpu']) && $server->cpu !== (int) $data['cpu']) {
$server->cpu = $data['cpu'];
$newBuild['cpu'] = (int) $server->cpu;
}
if (isset($data['io'])) {
if (isset($data['io']) && $server->io !== (int) $data['io']) {
$server->io = $data['io'];
$newBuild['io'] = (int) $server->io;
}
// Try save() here so if it fails we haven't contacted the daemon
// This won't be committed unless the HTTP request succeedes anyways
$server->save();
$newBuild['memory'] = (int) $server->memory;
$newBuild['swap'] = (int) $server->swap;
$newBuild['io'] = (int) $server->io;
$newBuild['cpu'] = (int) $server->cpu;
$newBuild['disk'] = (int) $server->disk;
if (!empty($newBuild)) {
$node = Models\Node::getByID($server->node);
$client = Models\Node::guzzleRequest($server->node);
@ -599,6 +634,7 @@ class ServerRepository
'build' => $newBuild
]
]);
}
DB::commit();
return true;
@ -732,11 +768,37 @@ class ServerRepository
public function deleteServer($id, $force)
{
$server = Models\Server::findOrFail($id);
$node = Models\Node::findOrFail($server->node);
DB::beginTransaction();
try {
// Delete Allocations
if ($force === 'force' || $force === true) {
$server->installed = 3;
$server->save();
}
$server->delete();
DB::commit();
event(new ServerDeleted($server->id));
} catch (\Exception $ex) {
DB::rollBack();
throw $ex;
}
}
public function deleteNow($id, $force = false) {
$server = Models\Server::withTrashed()->findOrFail($id);
$node = Models\Node::findOrFail($server->node);
// Handle server being restored previously or
// an accidental queue.
if (!$server->trashed()) {
return;
}
DB::beginTransaction();
try {
// Unassign Allocations
Models\Allocation::where('assigned_to', $server->id)->update([
'assigned_to' => null
]);
@ -744,15 +806,26 @@ class ServerRepository
// Remove Variables
Models\ServerVariables::where('server_id', $server->id)->delete();
// Remove Permissions (Foreign Key requires before Subusers)
Models\Permission::where('server_id', $server->id)->delete();
// Remove SubUsers
Models\Subuser::where('server_id', $server->id)->delete();
// Remove Permissions
Models\Permission::where('server_id', $server->id)->delete();
// Remove Downloads
Models\Download::where('server', $server->uuid)->delete();
// Clear Tasks
Models\Task::where('server', $server->id)->delete();
// Delete Databases
// This is the one un-recoverable point where
// transactions will not save us.
$repository = new DatabaseRepository;
foreach(Models\Database::select('id')->where('server_id', $server->id)->get() as &$database) {
$repository->drop($database->id);
}
$client = Models\Node::guzzleRequest($server->node);
$client->request('DELETE', '/servers', [
'headers' => [
@ -761,17 +834,16 @@ class ServerRepository
]
]);
$server->delete();
$server->forceDelete();
DB::commit();
return true;
} catch (\GuzzleHttp\Exception\TransferException $ex) {
if ($force === 'force') {
$server->delete();
// Set installed is set to 3 when force deleting.
if ($server->installed === 3 || $force) {
$server->forceDelete();
DB::commit();
return true;
} else {
DB::rollBack();
throw new DisplayException('An error occured while attempting to delete the server on the daemon.', $ex);
throw $ex;
}
} catch (\Exception $ex) {
DB::rollBack();
@ -779,6 +851,15 @@ class ServerRepository
}
}
public function cancelDeletion($id)
{
$server = Models\Server::withTrashed()->findOrFail($id);
$server->restore();
$server->installed = 1;
$server->save();
}
public function toggleInstall($id)
{
$server = Models\Server::findOrFail($id);
@ -794,9 +875,9 @@ class ServerRepository
* @param integer $id
* @return boolean
*/
public function suspend($id)
public function suspend($id, $deleted = false)
{
$server = Models\Server::findOrFail($id);
$server = ($deleted) ? Models\Server::withTrashed()->findOrFail($id) : Models\Server::findOrFail($id);
$node = Models\Node::findOrFail($server->node);
DB::beginTransaction();

View file

@ -71,6 +71,10 @@ class SubuserRepository
'download-files' => null,
'upload-files' => 's:files:upload',
'delete-files' => 's:files:delete',
'move-files' => 's:files:move',
'copy-files' => 's:files:copy',
'compress-files' => 's:files:compress',
'decompress-files' => 's:files:decompress',
// Subusers
'list-subusers' => null,

View file

@ -30,6 +30,7 @@ use Hash;
use Validator;
use Mail;
use Carbon;
use Auth;
use Pterodactyl\Models;
use Pterodactyl\Services\UuidService;
@ -51,18 +52,22 @@ class UserRepository
*
* @param string $email
* @param string|null $password An unhashed version of the user's password.
* @param bool $admin Boolean value if user should be an admin or not.
* @param int $token A custom user ID.
* @return bool|integer
*/
public function create($email, $password = null, $admin = false)
public function create($email, $password = null, $admin = false, $token = null)
{
$validator = Validator::make([
'email' => $email,
'password' => $password,
'root_admin' => $admin
'root_admin' => $admin,
'custom_id' => $token,
], [
'email' => 'required|email|unique:users,email',
'password' => 'nullable|regex:((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})',
'root_admin' => 'required|boolean'
'root_admin' => 'required|boolean',
'custom_id' => 'nullable|unique:users,id',
]);
// Run validator, throw catchable and displayable exception if it fails.
@ -77,6 +82,11 @@ class UserRepository
$user = new Models\User;
$uuid = new UuidService;
// Support for API Services
if (!is_null($token)) {
$user->id = $token;
}
$user->uuid = $uuid->generate('users', 'uuid');
$user->email = $email;
$user->password = Hash::make((is_null($password)) ? str_random(30) : $password);
@ -152,6 +162,11 @@ class UserRepository
throw new DisplayException('Cannot delete a user with active servers attached to thier account.');
}
// @TODO: this should probably be checked outside of this method because we won't always have Auth::user()
if(!is_null(Auth::user()) && Auth::user()->id === $id) {
throw new DisplayException('Cannot delete your own account.');
}
DB::beginTransaction();
try {

View file

@ -0,0 +1,64 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Services;
use Log;
use Illuminate\Http\Request;
use Pterodactyl\Models\APILog;
class APILogService
{
public function __constructor()
{
//
}
public static function log(Request $request, $error = null, $authorized = false)
{
if ($request->bearerToken() && !empty($request->bearerToken())) {
list($public, $hashed) = explode('.', $request->bearerToken());
} else {
$public = null;
}
try {
$log = APILog::create([
'authorized' => $authorized,
'error' => $error,
'key' => $public,
'method' => $request->method(),
'route' => $request->fullUrl(),
'content' => (empty($request->getContent())) ? null : $request->getContent(),
'user_agent' => $request->header('User-Agent'),
'request_ip' => $request->ip()
]);
$log->save();
} catch (\Exception $ex) {
// Simply log it and move on.
Log::error($ex);
}
}
}

View file

@ -0,0 +1,145 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Services;
use DB;
use Pterodactyl\Models;
use Pterodactyl\Exceptions\DisplayException;
class DeploymentService
{
public function __constructor()
{
//
}
/**
* Return a random location model. DO NOT USE.
* @return \Pterodactyl\Models\Node
*
* @TODO Actually make this smarter. If we're selecting a random location
* but then it has no nodes we should probably continue attempting all locations
* until we hit one.
*
* Currently you should just pick a location and go from there.
*/
public static function randomLocation()
{
return Models\Location::inRandomOrder()->first();
}
/**
* Return a model instance of a random node.
* @param int $location
* @param array $not
* @return \Pterodactyl\Models\Node
*
* @throws \Pterodactyl\Exceptions\DisplayException
*/
public static function randomNode($location, array $not = [])
{
$useLocation = Models\Location::where('id', $location)->first();
if (!$useLocation) {
throw new DisplayException("The location passed was not valid and could not be found.");
}
$node = Models\Node::where('location', $useLocation->id)->where('public', 1)->whereNotIn('id', $not)->inRandomOrder()->first();
if (!$node) {
throw new DisplayException("Unable to find a node in location {$useLocation->short} (id: {$useLocation->id}) that is available and has space.");
}
return $node;
}
/**
* Selects a random node ensuring it does not put the node
* over allocation limits.
* @param int $memory
* @param int $disk
* @param int $location
* @return \Pterodactyl\Models\Node;
*
* @throws \Pterodactyl\Exceptions\DisplayException
*/
public static function smartRandomNode($memory, $disk, $location = null) {
$node = self::randomNode($location);
$notIn = [];
do {
$return = self::checkNodeAllocation($node, $memory, $disk);
if (!$return) {
$notIn = array_merge($notIn, [
$node->id
]);
$node = self::randomNode($location, $notIn);
}
} while (!$return);
return $node;
}
/**
* Returns a random allocation for a node.
* @param int $node
* @return \Models\Pterodactyl\Allocation;
*/
public static function randomAllocation($node)
{
$allocation = Models\Allocation::where('node', $node)->whereNull('assigned_to')->inRandomOrder()->first();
if (!$allocation) {
throw new DisplayException('No available allocation could be found for the assigned node.');
}
return $allocation;
}
/**
* Checks that a node's allocation limits will not be passed with the given information.
* @param \Pterodactyl\Models\Node $node
* @param int $memory
* @param int $disk
* @return bool Returns true if this information would not put the node over it's limit.
*/
protected static function checkNodeAllocation(Models\Node $node, $memory, $disk)
{
if (is_numeric($node->memory_overallocate) || is_numeric($node->disk_overallocate)) {
$totals = Models\Server::select(DB::raw('SUM(memory) as memory, SUM(disk) as disk'))->where('node', $node->id)->first();
// Check memory limits
if (is_numeric($node->memory_overallocate)) {
$limit = ($node->memory * (1 + ($node->memory_overallocate / 100)));
$memoryLimitReached = (($totals->memory + $memory) > $limit);
}
// Check Disk Limits
if (is_numeric($node->disk_overallocate)) {
$limit = ($node->disk * (1 + ($node->disk_overallocate / 100)));
$diskLimitReached = (($totals->disk + $disk) > $limit);
}
return (!$diskLimitReached && !$memoryLimitReached);
}
}
}

View file

@ -0,0 +1,64 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Services;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\User;
use Pterodactyl\Notifications\Daemon;
class NotificationService {
protected $server;
protected $user;
/**
* Daemon will pass an event name, this matches that event name with the notification to send.
* @var array
*/
protected $types = [
// 'crashed' => 'CrashNotification',
// 'started' => 'StartNotification',
// 'stopped' => 'StopNotification',
// 'rebuild' => 'RebuildNotification'
];
public function __construct(Server $server)
{
$this->server = $server;
$this->user = User::findOrFail($server->owner);
}
public function pass(array $notification)
{
if (!$notification->type) {
return;
}
if (class_exists($this->types[$notification->type]::class)) {
$user->notify(new $this->types[$notification->type]($notification->payload));
}
}
}

View file

@ -8,28 +8,25 @@
"email": "dane@daneeveritt.com",
"homepage": "https://github.com/DaneEveritt",
"role": "Lead Developer"
},
{
"name": "Dylan Seidt",
"email": "dylan.seidt@gmail.com",
"role": "Developer"
}
],
"require": {
"php": ">=5.6.4",
"laravel/framework": "5.3.6",
"barryvdh/laravel-debugbar": "^2.2.3",
"doctrine/dbal": "^2.5.4",
"guzzlehttp/guzzle": "^6.2.1",
"pragmarx/google2fa": "^1.0.1",
"webpatser/laravel-uuid": "^2.0",
"prologue/alerts": "^0.4.0",
"s1lentium/iptools": "^1.1.0",
"edvinaskrucas/settings": "^2.0",
"igaster/laravel-theme": "^1.1.3",
"nesbot/carbon": "^1.21",
"mtdowling/cron-expression": "^1.1",
"dingo/api": "1.0.0-beta6"
"laravel/framework": "5.3.21",
"barryvdh/laravel-debugbar": "2.2.3",
"doctrine/dbal": "2.5.5",
"guzzlehttp/guzzle": "6.2.2",
"pragmarx/google2fa": "1.0.1",
"webpatser/laravel-uuid": "2.0.1",
"prologue/alerts": "0.4.0",
"s1lentium/iptools": "1.1.0",
"edvinaskrucas/settings": "2.0.0",
"igaster/laravel-theme": "1.1.3",
"nesbot/carbon": "1.21.0",
"mtdowling/cron-expression": "1.1.0",
"dingo/api": "1.0.0-beta6",
"aws/aws-sdk-php": "3.19.20",
"predis/predis": "1.1.1"
},
"require-dev": {
"fzaninotto/faker": "~1.4",

View file

@ -4,7 +4,7 @@ return [
'env' => env('APP_ENV', 'production'),
'version' => env('APP_VERSION', 'v0.4.1-beta'),
'version' => env('APP_VERSION', 'v0.5.0'),
/*
|--------------------------------------------------------------------------
@ -188,6 +188,8 @@ return [
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'DB' => Illuminate\Support\Facades\DB::class,
'Debugbar' => Barryvdh\Debugbar\Facade::class,
'Dingo' => Dingo\Api\Facade\API::class,
'DingoRoute'=> Dingo\Api\Facade\Route::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class,

View file

@ -38,38 +38,23 @@ return [
'database' => [
'driver' => 'database',
'table' => 'jobs',
'queue' => 'default',
'retry_after' => 60,
],
'beanstalkd' => [
'driver' => 'beanstalkd',
'host' => 'localhost',
'queue' => 'default',
'queue' => env('QUEUE_STANDARD', 'standard'),
'retry_after' => 60,
],
'sqs' => [
'driver' => 'sqs',
'key' => 'your-public-key',
'secret' => 'your-secret-key',
'queue' => 'your-queue-url',
'region' => 'us-east-1',
],
'iron' => [
'driver' => 'iron',
'host' => 'mq-aws-us-east-1.iron.io',
'token' => 'your-token',
'project' => 'your-project-id',
'queue' => 'your-queue-name',
'encrypt' => true,
'key' => env('SQS_KEY'),
'secret' => env('SQS_SECRET'),
'prefix' => env('SQS_QUEUE_PREFIX'),
'queue' => env('QUEUE_STANDARD', 'standard'),
'region' => env('SQS_REGION', 'us-east-1'),
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'default',
'queue' => env('QUEUE_STANDARD', 'standard'),
'retry_after' => 60,
],

View file

@ -29,9 +29,9 @@ return [
|
*/
'lifetime' => 120,
'lifetime' => 30,
'expire_on_close' => false,
'expire_on_close' => true,
/*
|--------------------------------------------------------------------------

View file

@ -20,7 +20,8 @@ class AddDockerImageColumn extends Migration
});
// Populate the column
$servers = Server::select(
DB::transaction(function () {
$servers = DB::table('servers')->select(
'servers.id',
'service_options.docker_image as s_optionImage'
)->join('service_options', 'service_options.id', '=', 'servers.option')->get();
@ -29,6 +30,7 @@ class AddDockerImageColumn extends Migration
$server->image = $server->s_optionImage;
$server->save();
}
});
}
/**

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class RenameDoubleInsurgency extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::transaction(function () {
$model = DB::table('service_options')->where('parent_service', 2)->where('id', 3)->where('name', 'Insurgency')->first();
if ($model) {
$model->name = 'Team Fortress 2';
$model->save();
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View file

@ -0,0 +1,40 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class BuildApiLogTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('api_logs', function (Blueprint $table) {
$table->increments('id');
$table->boolean('authorized');
$table->text('error')->nullable();
$table->char('key', 16)->nullable();
$table->char('method', 6);
$table->text('route');
$table->text('content')->nullable();
$table->text('user_agent');
$table->ipAddress('request_ip');
$table->timestampsTz();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('api_logs');
}
}

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class UpdateApiKeys extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('api_keys', function (Blueprint $table) {
$table->unsignedInteger('user')->after('id');
$table->text('memo')->after('allowed_ips')->nullable();
$table->timestamp('expires_at')->after('memo')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('api_keys', function (Blueprint $table) {
$table->dropColumn('user');
$table->dropColumn('memo');
$table->dropColumn('expires_at');
});
}
}

View file

@ -0,0 +1,30 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class UpdateMisnamedBungee extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::table('service_variables')->select('env_variable')->where('env_variable', 'BUNGE_VERSION')->update([
'env_variable' => 'BUNGEE_VERSION'
]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
return;
}
}

View file

@ -0,0 +1,65 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddForeignKeysServers extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::statement('ALTER TABLE servers
MODIFY COLUMN node INT(10) UNSIGNED NOT NULL,
MODIFY COLUMN owner INT(10) UNSIGNED NOT NULL,
MODIFY COLUMN allocation INT(10) UNSIGNED NOT NULL,
MODIFY COLUMN service INT(10) UNSIGNED NOT NULL,
MODIFY COLUMN servers.option INT(10) UNSIGNED NOT NULL
');
Schema::table('servers', function (Blueprint $table) {
$table->foreign('node')->references('id')->on('nodes');
$table->foreign('owner')->references('id')->on('users');
$table->foreign('allocation')->references('id')->on('allocations');
$table->foreign('service')->references('id')->on('services');
$table->foreign('option')->references('id')->on('service_options');
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('servers', function (Blueprint $table) {
$table->dropForeign('servers_node_foreign');
$table->dropForeign('servers_owner_foreign');
$table->dropForeign('servers_allocation_foreign');
$table->dropForeign('servers_service_foreign');
$table->dropForeign('servers_option_foreign');
$table->dropIndex('servers_node_foreign');
$table->dropIndex('servers_owner_foreign');
$table->dropIndex('servers_allocation_foreign');
$table->dropIndex('servers_service_foreign');
$table->dropIndex('servers_option_foreign');
$table->dropColumn('deleted_at');
});
DB::statement('ALTER TABLE servers
MODIFY COLUMN node MEDIUMINT(8) UNSIGNED NOT NULL,
MODIFY COLUMN owner MEDIUMINT(8) UNSIGNED NOT NULL,
MODIFY COLUMN allocation MEDIUMINT(8) UNSIGNED NOT NULL,
MODIFY COLUMN service MEDIUMINT(8) UNSIGNED NOT NULL,
MODIFY COLUMN servers.option MEDIUMINT(8) UNSIGNED NOT NULL
');
}
}

View file

@ -0,0 +1,47 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddForeignAllocations extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::statement('ALTER TABLE allocations
MODIFY COLUMN assigned_to INT(10) UNSIGNED NULL,
MODIFY COLUMN node INT(10) UNSIGNED NOT NULL
');
Schema::table('allocations', function (Blueprint $table) {
$table->foreign('assigned_to')->references('id')->on('servers');
$table->foreign('node')->references('id')->on('nodes');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('allocations', function (Blueprint $table) {
$table->dropForeign('allocations_assigned_to_foreign');
$table->dropForeign('allocations_node_foreign');
$table->dropIndex('allocations_assigned_to_foreign');
$table->dropIndex('allocations_node_foreign');
});
DB::statement('ALTER TABLE allocations
MODIFY COLUMN assigned_to MEDIUMINT(8) UNSIGNED NULL,
MODIFY COLUMN node MEDIUMINT(8) UNSIGNED NOT NULL
');
}
}

View file

@ -0,0 +1,33 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddForeignApiKeys extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('api_keys', function (Blueprint $table) {
$table->foreign('user')->references('id')->on('users');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('api_keys', function (Blueprint $table) {
$table->dropForeign('api_keys_user_foreign');
$table->dropIndex('api_keys_user_foreign');
});
}
}

View file

@ -0,0 +1,37 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddForeignApiPermissions extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::statement('ALTER TABLE api_permissions MODIFY key_id INT(10) UNSIGNED NOT NULL');
Schema::table('api_permissions', function (Blueprint $table) {
$table->foreign('key_id')->references('id')->on('api_keys');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('api_permissions', function (Blueprint $table) {
$table->dropForeign('api_permissions_key_id_foreign');
$table->dropIndex('api_permissions_key_id_foreign');
});
DB::statement('ALTER TABLE api_permissions MODIFY key_id MEDIUMINT(8) UNSIGNED NOT NULL');
}
}

View file

@ -0,0 +1,33 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddForeignDatabaseServers extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('database_servers', function (Blueprint $table) {
$table->foreign('linked_node')->references('id')->on('nodes');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('database_servers', function (Blueprint $table) {
$table->dropForeign('database_servers_linked_node_foreign');
$table->dropIndex('database_servers_linked_node_foreign');
});
}
}

View file

@ -0,0 +1,37 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddForeignDatabases extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('databases', function (Blueprint $table) {
$table->foreign('server_id')->references('id')->on('servers');
$table->foreign('db_server')->references('id')->on('database_servers');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('databases', function (Blueprint $table) {
$table->dropForeign('databases_server_id_foreign');
$table->dropForeign('databases_db_server_foreign');
$table->dropIndex('databases_server_id_foreign');
$table->dropIndex('databases_db_server_foreign');
});
}
}

View file

@ -0,0 +1,37 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddForeignNodes extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::statement('ALTER TABLE nodes MODIFY location INT(10) UNSIGNED NOT NULL');
Schema::table('nodes', function (Blueprint $table) {
$table->foreign('location')->references('id')->on('locations');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('nodes', function (Blueprint $table) {
$table->dropForeign('nodes_location_foreign');
$table->dropIndex('nodes_location_foreign');
});
DB::statement('ALTER TABLE nodes MODIFY location MEDIUMINT(10) UNSIGNED NOT NULL');
}
}

View file

@ -0,0 +1,38 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddForeignPermissions extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('permissions', function (Blueprint $table) {
$table->foreign('user_id')->references('id')->on('users');
$table->foreign('server_id')->references('id')->on('servers');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('permissions', function (Blueprint $table) {
$table->dropForeign('permissions_user_id_foreign');
$table->dropForeign('permissions_server_id_foreign');
$table->dropIndex('permissions_user_id_foreign');
$table->dropIndex('permissions_server_id_foreign');
});
}
}

View file

@ -0,0 +1,48 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddForeignServerVariables extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::statement('ALTER TABLE server_variables
MODIFY COLUMN server_id INT(10) UNSIGNED NULL,
MODIFY COLUMN variable_id INT(10) UNSIGNED NOT NULL
');
Schema::table('server_variables', function (Blueprint $table) {
$table->foreign('server_id')->references('id')->on('servers');
$table->foreign('variable_id')->references('id')->on('service_variables');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('server_variables', function (Blueprint $table) {
$table->dropForeign('server_variables_server_id_foreign');
$table->dropForeign('server_variables_variable_id_foreign');
$table->dropIndex('server_variables_server_id_foreign');
$table->dropIndex('server_variables_variable_id_foreign');
});
DB::statement('ALTER TABLE allocations
MODIFY COLUMN server_id MEDIUMINT(8) UNSIGNED NULL,
MODIFY COLUMN variable_id MEDIUMINT(8) UNSIGNED NOT NULL
');
}
}

View file

@ -0,0 +1,37 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddForeignServiceOptions extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::statement('ALTER TABLE service_options MODIFY parent_service INT(10) UNSIGNED NOT NULL');
Schema::table('service_options', function (Blueprint $table) {
$table->foreign('parent_service')->references('id')->on('services');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('service_options', function (Blueprint $table) {
$table->dropForeign('service_options_parent_service_foreign');
$table->dropIndex('service_options_parent_service_foreign');
});
DB::statement('ALTER TABLE service_options MODIFY parent_service MEDIUMINT(8) UNSIGNED NOT NULL');
}
}

View file

@ -0,0 +1,37 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddForeignServiceVariables extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::statement('ALTER TABLE service_variables MODIFY option_id INT(10) UNSIGNED NOT NULL');
Schema::table('service_variables', function (Blueprint $table) {
$table->foreign('option_id')->references('id')->on('service_options');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('service_variables', function (Blueprint $table) {
$table->dropForeign('service_variables_option_id_foreign');
$table->dropIndex('service_variables_option_id_foreign');
});
DB::statement('ALTER TABLE service_variables MODIFY option_id MEDIUMINT(8) UNSIGNED NOT NULL');
}
}

View file

@ -0,0 +1,37 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddForeignSubusers extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('subusers', function (Blueprint $table) {
$table->foreign('user_id')->references('id')->on('users');
$table->foreign('server_id')->references('id')->on('servers');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('subusers', function (Blueprint $table) {
$table->dropForeign('subusers_user_id_foreign');
$table->dropForeign('subusers_server_id_foreign');
$table->dropIndex('subusers_user_id_foreign');
$table->dropIndex('subusers_server_id_foreign');
});
}
}

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddForeignTasks extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('tasks', function (Blueprint $table) {
$table->foreign('server')->references('id')->on('servers');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('tasks', function (Blueprint $table) {
$table->dropForeign('tasks_server_foreign');
$table->dropForeign('tasks_server_foreign');
$table->dropIndex('tasks_server_foreign');
$table->dropIndex('tasks_server_foreign');
});
}
}

View file

@ -0,0 +1,97 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddArkServiceOptionFixed extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::transaction(function () {
$service = DB::table('services')->select('id')->where('author', 'ptrdctyl-v040-11e6-8b77-86f30ca893d3')->where('name', 'Source Engine')->first();
// No SRCDS Service, Skipping
if (!$service) {
return;
}
// Already have this service option installed.
if (DB::table('service_options')->select('id')->where('name', 'Ark: Survival Evolved')->where('parent_service', $service->id)->first()) {
return;
}
$oid = DB::table('service_options')->insertGetId([
'parent_service' => $service->id,
'name' => 'Ark: Survival Evolved',
'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! — Gamepedia: ARK',
'tag' => 'ark',
'docker_image' => 'quay.io/pterodactyl/srcds:ark',
'executable' => './ShooterGameServer',
'startup' => 'TheIsland?listen?ServerPassword={{ARK_PASSWORD}}?ServerAdminPassword={{ARK_ADMIN_PASSWORD}}?Port={{SERVER_PORT}}?MaxPlayers={{SERVER_MAX_PLAYERS}}'
]);
DB::table('service_variables')->insert([
'option_id' => $oid,
'name' => 'Server Password',
'description' => 'If specified, players must provide this password to join the server.',
'env_variable' => 'ARK_PASSWORD',
'default_value' => '',
'user_viewable' => 1,
'user_editable' => 1,
'required' => 0,
'regex' => '/^(\w\.*)$/'
]);
DB::table('service_variables')->insert([
'option_id' => $oid,
'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.',
'env_variable' => 'ARK_ADMIN_PASSWORD',
'default_value' => '',
'user_viewable' => 1,
'user_editable' => 1,
'required' => 0,
'regex' => '/^(\w\.*)$/'
]);
DB::table('service_variables')->insert([
'option_id' => $oid,
'name' => 'Maximum Players',
'description' => 'Specifies the maximum number of players that can play on the server simultaneously.',
'env_variable' => 'SERVER_MAX_PLAYERS',
'default_value' => 20,
'user_viewable' => 1,
'user_editable' => 1,
'required' => 1,
'regex' => '/^(\d{1,4})$/'
]);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
DB::transaction(function () {
$service = DB::table('services')->select('id')->where('author', 'ptrdctyl-v040-11e6-8b77-86f30ca893d3')->where('name', 'Source Engine')->first();
if ($service) {
$option = DB::table('service_options')->where('parent_service', $service->id)->where('tag', 'ark')->first();
if ($option) {
$variables = DB::table('service_variables')->where('option_id', $option->id)->delete();
}
}
});
}
}

View file

@ -215,7 +215,7 @@ class MinecraftServiceTableSeeder extends Seeder
'option_id' => $this->option['bungeecord']->id,
'name' => 'Bungeecord Version',
'description' => 'The version of Bungeecord to download and use.',
'env_variable' => 'BUNGE_VERSION',
'env_variable' => 'BUNGEE_VERSION',
'default_value' => 'latest',
'user_viewable' => 1,
'user_editable' => 1,

View file

@ -79,7 +79,7 @@ class SourceServiceTableSeeder extends Seeder
$this->option['tf2'] = Models\ServiceOptions::create([
'parent_service' => $this->service->id,
'name' => 'Insurgency',
'name' => 'Team Fortress 2',
'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.',
'tag' => 'srcds',
'docker_image' => 'quay.io/pterodactyl/srcds',
@ -87,6 +87,16 @@ class SourceServiceTableSeeder extends Seeder
'startup' => '-game {{SRCDS_GAME}} -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} -strictportbind -norestart'
]);
$this->option['ark'] = Models\ServiceOptions::create([
'parent_service' => $this->service->id,
'name' => 'Ark: Survival Evolved',
'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! — Gamepedia: ARK',
'tag' => 'ark',
'docker_image' => 'quay.io/pterodactyl/srcds:ark',
'executable' => './ShooterGameServer',
'startup' => 'TheIsland?listen?ServerPassword={{ARK_PASSWORD}}?ServerAdminPassword={{ARK_ADMIN_PASSWORD}}?Port={{SERVER_PORT}}?MaxPlayers={{SERVER_MAX_PLAYERS}}'
]);
$this->option['custom'] = Models\ServiceOptions::create([
'parent_service' => $this->service->id,
'name' => 'Custom Source Engine Game',
@ -102,6 +112,7 @@ class SourceServiceTableSeeder extends Seeder
{
$this->addInsurgencyVariables();
$this->addTF2Variables();
$this->addArkVariables();
$this->addCustomVariables();
}
@ -183,6 +194,45 @@ class SourceServiceTableSeeder extends Seeder
]);
}
private function addArkVariables()
{
DB::table('service_variables')->insert([
'option_id' => $this->option['ark']->id,
'name' => 'Server Password',
'description' => 'If specified, players must provide this password to join the server.',
'env_variable' => 'ARK_PASSWORD',
'default_value' => '',
'user_viewable' => 1,
'user_editable' => 1,
'required' => 0,
'regex' => '/^(\w\.*)$/'
]);
DB::table('service_variables')->insert([
'option_id' => $this->option['ark']->id,
'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.',
'env_variable' => 'ARK_ADMIN_PASSWORD',
'default_value' => '',
'user_viewable' => 1,
'user_editable' => 1,
'required' => 0,
'regex' => '/^(\w\.*)$/'
]);
DB::table('service_variables')->insert([
'option_id' => $this->option['ark']->id,
'name' => 'Maximum Players',
'description' => 'Specifies the maximum number of players that can play on the server simultaneously.',
'env_variable' => 'SERVER_MAX_PLAYERS',
'default_value' => 20,
'user_viewable' => 1,
'user_editable' => 1,
'required' => 1,
'regex' => '/^(\d{1,4})$/'
]);
}
private function addCustomVariables()
{
Models\ServiceVariables::create([

View file

@ -1,16 +0,0 @@
var elixir = require('laravel-elixir');
/*
|--------------------------------------------------------------------------
| Elixir Asset Management
|--------------------------------------------------------------------------
|
| Elixir provides a clean, fluent API for defining some basic Gulp tasks
| for your Laravel application. By default, we are compiling the Sass
| file for our application, as well as publishing vendor resources.
|
*/
elixir(function(mix) {
mix.sass('app.scss');
});

View file

@ -1,10 +0,0 @@
{
"private": true,
"devDependencies": {
"gulp": "^3.8.8"
},
"dependencies": {
"laravel-elixir": "^3.0.0",
"bootstrap-sass": "^3.0.0"
}
}

View file

@ -1,5 +0,0 @@
suites:
main:
namespace: Pterodactyl
psr4_prefix: Pterodactyl
src_path: app

View file

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="bootstrap/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false">
<testsuites>
<testsuite name="Application Test Suite">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">app/</directory>
</whitelist>
</filter>
<php>
<env name="APP_ENV" value="testing"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
</php>
</phpunit>

File diff suppressed because one or more lines are too long

View file

Before

Width:  |  Height:  |  Size: 357 KiB

After

Width:  |  Height:  |  Size: 357 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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