2015-12-14 03:22:16 +00:00
< ? php
namespace Pterodactyl\Repositories ;
use DB ;
2015-12-15 20:08:41 +00:00
use Debugbar ;
2015-12-14 03:22:16 +00:00
use Validator ;
2016-01-02 23:04:18 +00:00
use Log ;
2015-12-14 03:22:16 +00:00
use Pterodactyl\Models ;
use Pterodactyl\Services\UuidService ;
use Pterodactyl\Exceptions\DisplayException ;
use Pterodactyl\Exceptions\AccountNotFoundException ;
use Pterodactyl\Exceptions\DisplayValidationException ;
class ServerRepository
{
public function __construct ()
{
//
}
/**
* Generates a SFTP username for a server given a server name .
*
* @ param string $name
* @ return string
*/
protected function generateSFTPUsername ( $name )
{
$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 ))));
}
/**
* Adds a new server to the system .
2016-01-02 23:04:18 +00:00
* @ param array $data An array of data descriptors for creating the server . These should align to the columns in the database .
* @ return integer
2015-12-14 03:22:16 +00:00
*/
public function create ( array $data )
{
// Validate Fields
$validator = Validator :: make ( $data , [
'owner' => 'required|email|exists:users,email' ,
'node' => 'required|numeric|min:1|exists:nodes,id' ,
'name' => 'required|regex:([\w -]{4,35})' ,
'memory' => 'required|numeric|min:1' ,
2016-01-02 03:53:43 +00:00
'swap' => 'required|numeric|min:0' ,
2015-12-14 03:22:16 +00:00
'disk' => 'required|numeric|min:1' ,
'cpu' => 'required|numeric|min:0' ,
'io' => 'required|numeric|min:10|max:1000' ,
'ip' => 'required|ip' ,
'port' => 'required|numeric|min:1|max:65535' ,
'service' => 'required|numeric|min:1|exists:services,id' ,
'option' => 'required|numeric|min:1|exists:service_options,id' ,
2016-01-02 03:53:43 +00:00
'startup' => 'required' ,
2015-12-14 03:22:16 +00:00
'custom_image_name' => 'required_if:use_custom_image,on' ,
]);
// Run validator, throw catchable and displayable exception if it fails.
// Exception includes a JSON result of failed validation rules.
if ( $validator -> fails ()) {
2015-12-15 20:08:41 +00:00
throw new DisplayValidationException ( $validator -> errors ());
2015-12-14 03:22:16 +00:00
}
// 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 ();
// 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
$node = Models\Node :: find ( $data [ 'node' ]);
$allocation = Models\Allocation :: where ( 'ip' , $data [ 'ip' ]) -> where ( 'port' , $data [ 'port' ]) -> 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 ) {
throw new DisplayException ( 'The selected IP/Port combination (' . $data [ 'ip' ] . ':' . $data [ 'port' ] . ') is either already in use, or unavaliable for this node.' );
}
// Validate those Service Option Variables
// We know the service and option exists because of the validation.
// We need to verify that the option exists for the service, and then check for
// any required variable fields. (fields are labeled env_<env_variable>)
$option = Models\ServiceOptions :: where ( 'id' , $data [ 'option' ]) -> where ( 'parent_service' , $data [ 'service' ]) -> first ();
if ( ! $option ) {
throw new DisplayException ( 'The requested service option does not exist for the specified service.' );
}
// Check those Variables
$variables = Models\ServiceVariables :: where ( 'option_id' , $data [ 'option' ]) -> get ();
2015-12-15 20:08:41 +00:00
$variableList = [];
2015-12-14 03:22:16 +00:00
if ( $variables ) {
foreach ( $variables as $variable ) {
// Is the variable required?
if ( ! $data [ 'env_' . $variable -> env_variable ]) {
if ( $variable -> required === 1 ) {
throw new DisplayException ( 'A required service option variable field (env_' . $variable -> env_variable . ') was missing from the request.' );
}
2015-12-15 20:08:41 +00:00
$variableList = array_merge ( $variableList , [[
'var_id' => $variable -> id ,
'var_val' => $variable -> default_value
]]);
2015-12-14 03:22:16 +00:00
continue ;
}
// Check aganist Regex Pattern
if ( ! is_null ( $variable -> regex ) && ! preg_match ( $variable -> regex , $data [ 'env_' . $variable -> env_variable ])) {
throw new DisplayException ( 'Failed to validate service option variable field (env_' . $variable -> env_variable . ') aganist regex (' . $variable -> regex . ').' );
}
2015-12-15 20:08:41 +00:00
$variableList = array_merge ( $variableList , [[
'var_id' => $variable -> id ,
'var_val' => $data [ 'env_' . $variable -> env_variable ]
]]);
2015-12-14 03:22:16 +00:00
continue ;
2015-12-15 20:08:41 +00:00
}
}
// Check Overallocation
if ( is_numeric ( $node -> memory_overallocate ) || is_numeric ( $node -> disk_overallocate )) {
2015-12-14 03:22:16 +00:00
2015-12-15 20:08:41 +00:00
$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 )) {
$newMemory = $totals -> memory + $data [ 'memory' ];
$memoryLimit = ( $node -> memory * ( 1 + ( $node -> memory_overallocate / 100 )));
if ( $newMemory > $memoryLimit ) {
throw new DisplayException ( 'The amount of memory allocated to this server would put the node over its allocation limits. This node is allowed ' . ( $node -> memory_overallocate + 100 ) . '% of its assigned ' . $node -> memory . 'Mb of memory (' . $memoryLimit . 'Mb) of which ' . (( $totals -> memory / $node -> memory ) * 100 ) . '% (' . $totals -> memory . 'Mb) is in use already. By allocating this server the node would be at ' . (( $newMemory / $node -> memory ) * 100 ) . '% (' . $newMemory . 'Mb) usage.' );
}
2015-12-14 03:22:16 +00:00
}
2015-12-15 20:08:41 +00:00
// Check Disk Limits
if ( is_numeric ( $node -> disk_overallocate )) {
$newDisk = $totals -> disk + $data [ 'disk' ];
$diskLimit = ( $node -> disk * ( 1 + ( $node -> disk_overallocate / 100 )));
if ( $newDisk > $diskLimit ) {
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 ();
$uuid = new UuidService ;
// Add Server to the Database
$server = new Models\Server ;
2016-01-02 03:53:43 +00:00
$generatedUuid = $uuid -> generate ( 'servers' , 'uuid' );
2015-12-15 20:08:41 +00:00
$server -> fill ([
2016-01-02 03:53:43 +00:00
'uuid' => $generatedUuid ,
'uuidShort' => $uuid -> generateShort ( 'servers' , 'uuidShort' , $generatedUuid ),
2015-12-15 20:08:41 +00:00
'node' => $data [ 'node' ],
'name' => $data [ 'name' ],
'active' => 1 ,
'owner' => $user -> id ,
'memory' => $data [ 'memory' ],
2016-01-02 03:53:43 +00:00
'swap' => $data [ 'swap' ],
2015-12-15 20:08:41 +00:00
'disk' => $data [ 'disk' ],
'io' => $data [ 'io' ],
'cpu' => $data [ 'cpu' ],
2016-01-02 03:53:43 +00:00
'oom_disabled' => ( isset ( $data [ 'oom_disabled' ])) ? true : false ,
2015-12-15 20:08:41 +00:00
'ip' => $data [ 'ip' ],
'port' => $data [ 'port' ],
'service' => $data [ 'service' ],
'option' => $data [ 'option' ],
2016-01-02 03:53:43 +00:00
'startup' => $data [ 'startup' ],
2015-12-15 20:08:41 +00:00
'daemonSecret' => $uuid -> generate ( 'servers' , 'daemonSecret' ),
'username' => $this -> generateSFTPUsername ( $data [ 'name' ])
]);
$server -> save ();
// Mark Allocation in Use
$allocation -> assigned_to = $server -> id ;
$allocation -> save ();
// Add Variables
foreach ( $variableList as $item ) {
Models\ServerVariables :: create ([
'server_id' => $server -> id ,
'variable_id' => $item [ 'var_id' ],
'variable_value' => $item [ 'var_val' ]
]);
2015-12-14 03:22:16 +00:00
}
2015-12-15 20:08:41 +00:00
try {
// Add logic for communicating with Wings to make the server in here.
// We should add the server regardless of the Wings response, but
// handle the error and then allow the server to be re-deployed.
DB :: commit ();
return $server -> id ;
} catch ( \Exception $e ) {
DB :: rollBack ();
throw $e ;
}
2015-12-14 03:22:16 +00:00
}
2016-01-02 23:04:18 +00:00
/**
* [ updateDetails description ]
* @ param integer $id
* @ param array $data
* @ return boolean
*/
public function updateDetails ( $id , array $data )
{
$uuid = new UuidService ;
$resetDaemonKey = false ;
// Validate Fields
$validator = Validator :: make ( $data , [
'owner' => 'email|exists:users,email' ,
2016-01-03 04:21:22 +00:00
'name' => 'regex:^([\w -]{4,35})$'
2016-01-02 23:04:18 +00:00
]);
// 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 ());
}
DB :: beginTransaction ();
$server = Models\Server :: findOrFail ( $id );
$owner = Models\User :: findOrFail ( $server -> owner );
// Update daemon secret if it was passed.
if (( isset ( $data [ 'reset_token' ]) && $data [ 'reset_token' ] === true ) || ( isset ( $data [ 'owner' ]) && $data [ 'owner' ] !== $owner -> email )) {
$oldDaemonKey = $server -> daemonSecret ;
$server -> daemonSecret = $uuid -> generate ( 'servers' , 'daemonSecret' );
$resetDaemonKey = true ;
}
// Update Server Owner if it was passed.
if ( isset ( $data [ 'owner' ]) && $data [ 'owner' ] !== $owner -> email ) {
$newOwner = Models\User :: select ( 'id' ) -> where ( 'email' , $data [ 'owner' ]) -> first ();
$server -> owner = $newOwner -> id ;
}
// Update Server Name if it was passed.
if ( isset ( $data [ 'name' ])) {
$server -> name = $data [ 'name' ];
}
// Save our changes
$server -> save ();
// Do we need to update? If not, return successful.
if ( ! $resetDaemonKey ) {
DB :: commit ();
return true ;
}
// If we need to update do it here.
try {
$node = Models\Node :: getByID ( $server -> node );
$client = Models\Node :: guzzleRequest ( $server -> node );
$res = $client -> request ( 'PATCH' , '/server' , [
'headers' => [
'X-Access-Server' => $server -> uuid ,
'X-Access-Token' => $node -> daemonSecret
],
'exceptions' => false ,
'json' => [
'keys' => [
( string ) $oldDaemonKey => [],
( string ) $server -> daemonSecret => [
's:get' ,
's:power' ,
's:console' ,
's:command' ,
's:files:get' ,
's:files:read' ,
's:files:post' ,
's:files:delete' ,
's:files:upload' ,
's:set-password'
]
]
]
]);
if ( $res -> getStatusCode () === 204 ) {
DB :: commit ();
return true ;
} else {
throw new DisplayException ( 'Daemon returned a a non HTTP/204 error code. HTTP/' + $res -> getStatusCode ());
}
} catch ( \Exception $ex ) {
2016-01-03 04:21:22 +00:00
DB :: rollBack ();
2016-01-02 23:04:18 +00:00
Log :: error ( $ex );
throw new DisplayException ( 'An error occured while attempting to update this server\'s information.' );
}
}
2016-01-03 04:21:22 +00:00
/**
* [ changeBuild description ]
* @ param integer $id
* @ param array $data
* @ return boolean
*/
public function changeBuild ( $id , array $data )
{
$validator = Validator :: make ( $data , [
'default' => [
'string' ,
'regex:/^(\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5]))\.(\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5]))\.(\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5]))\.(\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5])):(\d{1,5})$/'
],
'add_additional' => 'array' ,
'remove_additional' => 'array' ,
'memory' => 'integer|min:0' ,
'swap' => 'integer|min:0' ,
'io' => 'integer|min:10|max:1000' ,
'cpu' => 'integer|min:0' ,
'disk' => 'integer|min:0'
]);
// 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 ());
}
DB :: beginTransaction ();
$server = Models\Server :: findOrFail ( $id );
if ( isset ( $data [ 'default' ])) {
list ( $ip , $port ) = explode ( ':' , $data [ 'default' ]);
if ( $ip === $server -> ip && $port === $server -> port ) {
continue ;
}
$allocation = Models\Allocation :: where ( 'ip' , $ip ) -> where ( 'port' , $port ) -> where ( 'assigned_to' , $server -> id ) -> get ();
if ( ! $allocation ) {
throw new DisplayException ( 'The assigned default connection (' . $ip . ':' . $prot . ') is not allocated to this server.' );
}
$server -> ip = $ip ;
$server -> port = $port ;
}
// Remove Assignments
if ( isset ( $data [ 'remove_additional' ])) {
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 ;
}
// Can't remove the assigned IP/Port combo
if ( $ip === $server -> ip && $port === $server -> port ) {
continue ;
}
Models\Allocation :: where ( 'ip' , $ip ) -> where ( 'port' , $port ) -> where ( 'assigned_to' , $server -> id ) -> update ([
'assigned_to' => null
]);
}
}
// Add Assignments
if ( isset ( $data [ 'add_additional' ])) {
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 ;
}
// Don't allow double port assignments
if ( Models\Allocation :: where ( 'port' , $port ) -> where ( 'assigned_to' , $server -> id ) -> count () !== 0 ) {
continue ;
}
Models\Allocation :: where ( 'ip' , $ip ) -> where ( 'port' , $port ) -> whereNull ( 'assigned_to' ) -> update ([
'assigned_to' => $server -> id
]);
}
}
// Loop All Assignments
$additionalAssignments = [];
$assignments = Models\Allocation :: where ( 'assigned_to' , $server -> id ) -> get ();
foreach ( $assignments as & $assignment ) {
if ( array_key_exists (( string ) $assignment -> ip , $additionalAssignments )) {
array_push ( $additionalAssignments [ ( string ) $assignment -> ip ], ( int ) $assignment -> port );
} else {
$additionalAssignments [ ( string ) $assignment -> ip ] = [ ( int ) $assignment -> port ];
}
}
// @TODO: verify that server can be set to this much memory without
// going over node limits.
if ( isset ( $data [ 'memory' ])) {
$server -> memory = $data [ 'memory' ];
}
if ( isset ( $data [ 'swap' ])) {
$server -> swap = $data [ 'swap' ];
}
// @TODO: verify that server can be set to this much disk without
// going over node limits.
if ( isset ( $data [ 'disk' ])) {
$server -> disk = $data [ 'disk' ];
}
if ( isset ( $data [ 'cpu' ])) {
$server -> cpu = $data [ 'cpu' ];
}
if ( isset ( $data [ 'io' ])) {
$server -> io = $data [ 'io' ];
}
try {
$node = Models\Node :: getByID ( $server -> node );
$client = Models\Node :: guzzleRequest ( $server -> node );
$client -> request ( 'PATCH' , '/server' , [
'headers' => [
'X-Access-Server' => $server -> uuid ,
'X-Access-Token' => $node -> daemonSecret
],
'json' => [
'build' => [
'default' => [
'ip' => $server -> ip ,
'port' => ( int ) $server -> port
],
'ports|overwrite' => $additionalAssignments ,
'memory' => ( int ) $server -> memory ,
'swap' => ( int ) $server -> swap ,
'io' => ( int ) $server -> io ,
'cpu' => ( int ) $server -> cpu ,
'disk' => ( int ) $server -> disk
]
]
]);
$server -> save ();
DB :: commit ();
return true ;
} catch ( \GuzzleHttp\Exception\TransferException $ex ) {
DB :: rollBack ();
throw new DisplayException ( 'An error occured while attempting to update the configuration: ' . $ex -> getMessage ());
}
}
2015-12-14 03:22:16 +00:00
}