Merge branch 'develop' into feature/service-changes

This commit is contained in:
Dane Everitt 2016-11-27 14:01:13 -05:00
commit 9eb14614c2
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
20 changed files with 345 additions and 70 deletions

View file

@ -3,6 +3,27 @@ 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.5.2 (Bodacious Boreopterus)
### Fixed
* Time axis on server graphs is corrected to show the minutes rather than the current month.
* Node deletion now works correctly and deletes allocations as well.
* Fixes a bug that would leave orphaned databases on the system if there was an error during creation.
* Fixes an issue that could occur if a UUID contained `#e#` formatting within it when it comes to creating databases.
* Fixed node status display to account for updated daemon security changes.
* Fixes default language being selected as German (defaults to English now).
* Fixes bug preventing the deletion of database servers.
### Changed
* Using `node:<name>` when filtering servers now properly filters the servers by node name, rather than looking for the node ID.
* Using `owner:<email>` when filtering servers now properly filters by the owner's email rather than ID.
* Added some quick help buttons to the admin index page for getting support or checking the documentation.
* Panel now displays `Pterodactyl Panel` as the company name if one is not set.
### Added
* Added basic information about the daemon when viewing a node, including the host OS and version, CPU count, and the daemon version.
* Added version checking for the daemon and panel that alerts admins when daemons or the panel is out of date.
* Added multiplicator support to certain memory and disk fields that allow users to enter `10g` and have it converted to MB automatically.
## v0.5.1 (Bodacious Boreopterus)
### Fixed
* Fixes a bug that allowed a user to bypass 2FA authentication if using the correct username and password for an account.

View file

@ -24,6 +24,7 @@
namespace Pterodactyl\Console\Commands;
use Illuminate\Console\Command;
use Version;
class ShowVersion extends Command
{
@ -58,6 +59,6 @@ class ShowVersion extends Command
*/
public function handle()
{
$this->info('You are running Pterodactyl Panel ' . config('app.version'));
$this->info('You are running Pterodactyl Panel v' . Version::getCurrentPanel() . ' (' . ((Version::isLatestPanel()) ? 'Up to Date' : 'Latest: ' . Version::getDaemon()) . ')');
}
}

35
app/Facades/Version.php Normal file
View file

@ -0,0 +1,35 @@
<?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\Facades;
use Illuminate\Support\Facades\Facade;
class Version extends Facade
{
protected static function getFacadeAccessor()
{
return '\Pterodactyl\Services\VersionService';
}
}

View file

@ -253,20 +253,22 @@ class NodesController extends Controller
public function deleteNode(Request $request, $id)
{
$node = Models\Node::findOrFail($id);
$servers = Models\Server::where('node', $id)->count();
if ($servers > 0) {
Alert::danger('You cannot delete a node with servers currently attached to it.')->flash();
try {
$repo = new NodeRepository;
$repo->delete($id);
Alert::success('Successfully deleted the requested node from the panel.')->flash();
return redirect()->route('admin.nodes');
} catch (DisplayException $e) {
Alert::danger($e->getMessage())->flash();
} catch (\Exception $e) {
Log::error($e);
Alert::danger('An unhandled exception occured while attempting to delete this node. Please try again.')->flash();
}
return redirect()->route('admin.nodes.view', [
'id' => $id,
'tab' => 'tab_delete'
]);
}
$node->delete();
Alert::success('Node successfully deleted.')->flash();
return redirect()->route('admin.nodes');
}
}

View file

@ -68,14 +68,23 @@ class ServersController extends Controller
$match = str_replace('"', '', $match);
if (strpos($match, ':')) {
list($field, $term) = explode(':', $match);
$field = (strpos($field, '.')) ? $field : 'servers.' . $field;
if ($field === 'node') {
$field = 'nodes.name';
} else if ($field === 'owner') {
$field = 'users.email';
} else if (!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 . '%');
$query->orWhere([
['servers.username', 'LIKE', '%' . $match . '%'],
['users.email', 'LIKE', '%' . $match . '%'],
['allocations.port', 'LIKE', '%' . $match . '%'],
['allocations.ip', 'LIKE', '%' . $match . '%'],
]);
}
}
}

View file

@ -56,13 +56,12 @@ class DatabaseRepository {
}
DB::beginTransaction();
try {
$db = new Models\Database;
$db->fill([
'server_id' => $server->id,
'db_server' => $options['db_server'],
'database' => $server->uuidShort . '_' . $options['database'],
'database' => "s{$server->id}_{$options['database']}",
'username' => $server->uuidShort . '_' . str_random(7),
'remote' => $options['remote'],
'password' => Crypt::encrypt(str_random(20))
@ -90,18 +89,31 @@ class DatabaseRepository {
$capsule->setAsGlobal();
Capsule::statement('CREATE DATABASE ' . $db->database);
Capsule::statement('CREATE USER \'' . $db->username . '\'@\'' . $db->remote . '\' IDENTIFIED BY \'' . Crypt::decrypt($db->password) . '\'');
Capsule::statement('GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX ON ' . $db->database . '.* TO \'' . $db->username . '\'@\'' . $db->remote . '\'');
Capsule::statement('FLUSH PRIVILEGES');
DB::commit();
return true;
} catch (\Exception $ex) {
DB::rollback();
DB::rollBack();
throw new DisplayException('There was an error while connecting to the Database Host Server. Please check the error logs.', $ex);
}
try {
Capsule::statement('CREATE DATABASE `' . $db->database . '`');
Capsule::statement('CREATE USER `' . $db->username . '`@`' . $db->remote . '` IDENTIFIED BY \'' . Crypt::decrypt($db->password) . '\'');
Capsule::statement('GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX ON `' . $db->database . '`.* TO `' . $db->username . '`@`' . $db->remote . '`');
Capsule::statement('FLUSH PRIVILEGES');
DB::commit();
} catch (\Exception $ex) {
try {
Capsule::statement('DROP DATABASE `' . $db->database . '`');
Capsule::statement('DROP USER `' . $db->username . '`@`' . $db->remote . '`');
} catch (\Exception $exi) {
// ignore it, if it fails its probably
// because we failed to ever make the DB
// or the user on the system.
} finally {
DB::rollBack();
throw $ex;
}
}
}
/**
* Updates the password for a given database.
@ -138,7 +150,7 @@ class DatabaseRepository {
$capsule->setAsGlobal();
Capsule::statement(sprintf(
'SET PASSWORD FOR \'%s\'@\'%s\' = PASSWORD(\'%s\')',
'SET PASSWORD FOR `%s`@`%s` = PASSWORD(\'%s\')',
$db->username,
$db->remote,
$password
@ -182,8 +194,8 @@ class DatabaseRepository {
$capsule->setAsGlobal();
Capsule::statement('DROP USER \'' . $db->username . '\'@\'' . $db->remote . '\'');
Capsule::statement('DROP DATABASE ' . $db->database);
Capsule::statement('DROP USER `' . $db->username . '`@`' . $db->remote . '`');
Capsule::statement('DROP DATABASE `' . $db->database . '`');
$db->delete();
@ -219,6 +231,11 @@ class DatabaseRepository {
*/
public function add(array $data)
{
if (isset($data['host'])) {
$data['host'] = gethostbyname($data['host']);
}
$validator = Validator::make($data, [
'name' => 'required|string|max:255',
'host' => 'required|ip|unique:database_servers,host',

View file

@ -229,8 +229,30 @@ class NodeRepository {
public function delete($id)
{
// @TODO: add logic;
return true;
$node = Models\Node::findOrFail($id);
if (Models\Server::where('node', $id)->count() > 0) {
throw new DisplayException('You cannot delete a node with servers currently attached to it.');
}
DB::beginTransaction();
try {
// Unlink Database Servers
Models\DatabaseServer::where('linked_node', $node->id)->update([
'linked_node' => null,
]);
// Delete Allocations
Models\Allocation::where('node', $node->id)->delete();
// Delete Node
$node->delete();
DB::commit();
} catch (\Exception $ex) {
DB::rollback();
throw $ex;
}
}
}

View file

@ -0,0 +1,93 @@
<?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 Cache;
use GuzzleHttp\Client;
class VersionService
{
protected static $versions;
/**
* Constructor
*/
public function __construct()
{
self::$versions = Cache::remember('versions', env('VERSION_CACHE_TIME', 60), function () {
$client = new Client();
try {
$response = $client->request('GET', env('VERSION_CHECK_URL', 'https://cdn.pterodactyl.io/releases/latest.json'));
if ($response->getStatusCode() === 200) {
return json_decode($response->getBody());
} else {
throw new \Exception('Invalid response code.');
}
} catch (\Exception $ex) {
// Failed request, just return errored version.
return (object) [
'panel' => 'error',
'daemon' => 'error',
];
}
});
}
public static function getPanel()
{
return self::$versions->panel;
}
public static function getDaemon()
{
return self::$versions->daemon;
}
public function getCurrentPanel()
{
return config('app.version');
}
public static function isLatestPanel()
{
if (config('app.version') === 'canary') {
return true;
}
return (version_compare(config('app.version'), self::$versions->panel) >= 0);
}
public static function isLatestDaemon($daemon)
{
if ($daemon === '0.0.0-canary') {
return true;
}
return (version_compare($daemon, self::$versions->daemon) >= 0);
}
}

View file

@ -217,6 +217,7 @@ return [
'URL' => Illuminate\Support\Facades\URL::class,
'Uuid' => Webpatser\Uuid\Uuid::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'Version' => Pterodactyl\Facades\Version::class,
'View' => Illuminate\Support\Facades\View::class,
],

View file

@ -46,4 +46,24 @@ $(document).ready(function () {
centerModal($(this));
});
$(window).on('resize', centerModal);
// Idea code for multiplicators submitted by @Taronyuu on Github
// https://github.com/Pterodactyl/Panel/issues/154#issuecomment-257116078
$('input[data-multiplicator="true"]').on('change', function () {
var value = $(this).val();
if (!/^\d+$/.test(value)) {
var multiplicator = value.replace(/[0-9]/g, '').toLowerCase();
value = value.replace(/\D/g, '');
if (multiplicator === 't') {
value = value * (1024 * 1024);
}
if (multiplicator === 'g') {
value = value * 1024;
}
}
$(this).val(value);
});
});

View file

@ -93,7 +93,7 @@
<td>{{ $db->username }}</td>
<td class="text-center">{{ $db->c_databases }}</td>
<td>@if(is_null($db->a_linkedNode))<em>unlinked</em>@else{{ $db->a_linkedNode }}@endif</td>
<td class="text-center"><a href="#" class="text-danger" data-action="delete" data-type="delete-dbserver" data-attr="{{ $db->id }}"><i class="fa fa-trash-o"></i></a></td>
<td class="text-center"><a href="#" class="text-danger" data-action="delete" data-type="delete-server" data-attr="{{ $db->id }}"><i class="fa fa-trash-o"></i></a></td>
</tr>
@endforeach
</tbody>

View file

@ -24,13 +24,31 @@
@endsection
@section('content')
<div class="row">
<div class="col-md-12">
<ul class="breadcrumb">
<li class="active">Admin Control</li>
</ul>
<h3 class="nopad">Pterodactyl Admin Control Panel</h3><hr />
<p>Welcome to the most advanced, lightweight, and user-friendly open source game server control panel.</p>
<p>You are running version <code>{{ config('app.version') }}</code>.</p>
@if (Version::isLatestPanel())
<div class="alert alert-success">You are running Pterodactyl Panel version <code>{{ Version::getCurrentPanel() }}</code>. Your panel is up-to-date!</div>
@else
<div class="alert alert-danger">
Your panel is <strong>not up-to-date!</strong> The latest version is <a href="https://github.com/Pterodactyl/Panel/releases/v{{ Version::getPanel() }}" target="_blank"><code>{{ Version::getPanel() }}</code></a> and you are currently running version <code>{{ Version::getCurrentPanel() }}</code>.
</div>
@endif
</div>
</div>
<div class="row">
<div class="col-xs-4 text-center">
<a href="https://discord.gg/0gYt8oU8QOkDhKLS"><button class="btn btn-sm btn-warning" style="width:100%;"><i class="fa fa-fw fa-support"></i> Get Help <small>(via Discord)</small></button></a>
</div>
<div class="col-xs-4 text-center">
<a href="https://docs.pterodactyl.io"><button class="btn btn-sm btn-default" style="width:100%;"><i class="fa fa-fw fa-link"></i> Documentation</button></a>
</div>
<div class="col-xs-4 text-center">
<a href="https://github.com/Pterodactyl/Panel"><button class="btn btn-sm btn-default" style="width:100%;"><i class="fa fa-fw fa-support"></i> Github</button></a>
</div>
</div>
<script>
$(document).ready(function () {

View file

@ -70,21 +70,27 @@
<script>
$(document).ready(function () {
$('#sidebar_links').find("a[href='/admin/nodes']").addClass('active');
pingNodes();
setInterval(pingNodes, 10000);
});
function pingNodes() {
(function pingNodes() {
$('td[data-action="ping"]').each(function(i, element) {
$.ajax({
type: 'GET',
url: $(element).data('location'),
headers: {
'X-Access-Token': '{{ $node->daemonSecret }}'
},
timeout: 5000
}).done(function (data) {
$(element).find('i').tooltip({
title: 'v' + data.version,
});
$(element).removeClass('text-muted').find('i').removeClass().addClass('fa fa-fw fa-heartbeat faa-pulse animated').css('color', '#50af51');
}).fail(function () {
$(element).removeClass('text-muted').find('i').removeClass().addClass('fa fa-fw fa-heart-o').css('color', '#d9534f');
}).always(function () {
setTimeout(pingNodes, 10000);
});
});
}
})();
});
</script>
@endsection

View file

@ -92,14 +92,14 @@
<div class="form-group col-md-6 col-xs-6">
<label for="memory" class="control-label">Total Memory</label>
<div class="input-group">
<input type="text" name="memory" class="form-control" value="{{ old('memory') }}"/>
<input type="text" name="memory" data-multiplicator="true" class="form-control" value="{{ old('memory') }}"/>
<span class="input-group-addon">MB</span>
</div>
</div>
<div class="form-group col-md-6 col-xs-6">
<label for="memory_overallocate" class="control-label">Overallocate</label>
<div class="input-group">
<input type="text" name="memory_overallocate" class="form-control" value="{{ old('memory_overallocate', 0) }}"/>
<input type="text" name="memory_overallocate" data-multiplicator="true" class="form-control" value="{{ old('memory_overallocate', 0) }}"/>
<span class="input-group-addon">%</span>
</div>
</div>

View file

@ -69,6 +69,18 @@
<div class="panel-body">
<table class="table table-striped" style="margin-bottom:0;">
<tbody>
<tr>
<td>Daemon Version</td>
<td><code data-attr="info-version"><i class="fa fa-refresh fa-fw fa-spin"></i></code> (Latest: <code>{{ Version::getPanel() }}</code>)</td>
</tr>
<tr>
<td>System Information</td>
<td data-attr="info-system"><i class="fa fa-refresh fa-fw fa-spin"></i></td>
</tr>
<tr>
<td>Total CPU Cores</td>
<td data-attr="info-cpus"><i class="fa fa-refresh fa-fw fa-spin"></i></td>
</tr>
<tr>
<td>Total Servers</td>
<td>{{ count($servers) }}</td>
@ -171,7 +183,7 @@
<div class="form-group col-md-3 col-xs-6">
<label for="memory" class="control-label">Total Memory</label>
<div class="input-group">
<input type="text" name="memory" class="form-control" value="{{ old('memory', $node->memory) }}"/>
<input type="text" name="memory" class="form-control" data-multiplicator="true" value="{{ old('memory', $node->memory) }}"/>
<span class="input-group-addon">MB</span>
</div>
</div>
@ -185,7 +197,7 @@
<div class="form-group col-md-3 col-xs-6">
<label for="disk" class="control-label">Disk Space</label>
<div class="input-group">
<input type="text" name="disk" class="form-control" value="{{ old('disk', $node->disk) }}"/>
<input type="text" name="disk" class="form-control" data-multiplicator="true" value="{{ old('disk', $node->disk) }}"/>
<span class="input-group-addon">MB</span>
</div>
</div>
@ -777,6 +789,24 @@ $(document).ready(function () {
element.parent().removeClass('has-error has-success');
}
(function getInformation() {
$.ajax({
method: 'GET',
url: '{{ $node->scheme }}://{{ $node->fqdn }}:{{ $node->daemonListen }}',
timeout: 5000,
headers: {
'X-Access-Token': '{{ $node->daemonSecret }}'
},
}).done(function (data) {
$('[data-attr="info-version"]').html(data.version);
$('[data-attr="info-system"]').html(data.system.type + '(' + data.system.arch + ') <code>' + data.system.release + '</code>');
$('[data-attr="info-cpus"]').html(data.system.cpus);
}).fail(function (jqXHR) {
}).always(function() {
setTimeout(getInformation, 10000);
});
})();
});
</script>
@endsection

View file

@ -118,14 +118,14 @@
<div class="form-group col-md-4 col-xs-4">
<label for="memory" class="control-label">Memory</label>
<div class="input-group">
<input type="text" name="memory" class="form-control" value="{{ old('memory') }}"/>
<input type="text" name="memory" data-multiplicator="true" class="form-control" value="{{ old('memory') }}"/>
<span class="input-group-addon">MB</span>
</div>
</div>
<div class="form-group col-md-4 col-xs-4">
<label for="memory" class="control-label">Swap</label>
<div class="input-group">
<input type="text" name="swap" class="form-control" value="{{ old('swap', 0) }}"/>
<input type="text" name="swap" data-multiplicator="true" class="form-control" value="{{ old('swap', 0) }}"/>
<span class="input-group-addon">MB</span>
</div>
</div>
@ -150,7 +150,7 @@
<div class="form-group col-md-4 col-xs-4">
<label for="disk" class="control-label">Disk Space</label>
<div class="input-group">
<input type="text" name="disk" class="form-control" value="{{ old('disk') }}"/>
<input type="text" name="disk" data-multiplicator="true" class="form-control" value="{{ old('disk') }}"/>
<span class="input-group-addon">MB</span>
</div>
</div>

View file

@ -228,14 +228,14 @@
<div class="col-md-6 form-group {{ $errors->has('memory') ? 'has-error' : '' }}">
<label for="memory" class="control-label">Allocated Memory</label>
<div class="input-group">
<input type="text" name="memory" class="form-control" value="{{ old('memory', $server->memory) }}"/>
<input type="text" name="memory" data-multiplicator="true" class="form-control" value="{{ old('memory', $server->memory) }}"/>
<span class="input-group-addon">MB</span>
</div>
</div>
<div class="col-md-6 form-group {{ $errors->has('swap') ? 'has-error' : '' }}">
<label for="swap" class="control-label">Allocated Swap</label>
<div class="input-group">
<input type="text" name="swap" class="form-control" value="{{ old('swap', $server->swap) }}"/>
<input type="text" name="swap" data-multiplicator="true" class="form-control" value="{{ old('swap', $server->swap) }}"/>
<span class="input-group-addon">MB</span>
</div>
<p class="text-muted"><small>Setting this to <code>0</code> will disable swap space on this server.</small></p>
@ -373,7 +373,7 @@
<div class="form-group col-md-6">
<label class="control-label">Database Name:</label>
<div class="input-group">
<div class="input-group-addon">{{ $server->uuidShort }}_</div>
<div class="input-group-addon">s{{ $server->id }}_</div>
<input type="text" name="database" value="{{ old('database') }}" class="form-control">
</div>
</div>

View file

@ -44,7 +44,7 @@
<div>
<select name="default_language" class="form-control">
<option value="de" @if(Settings::get('default_language') === 'de')selected @endif>Deutsch</option>
<option value="en" @if(Settings::get('default_language') === 'en')selected @endif>English</option>
<option value="en" @if(Settings::get('default_language', 'en') === 'en')selected @endif>English</option>
<option value="es" @if(Settings::get('default_language') === 'es')selected @endif>Espa&ntilde;ol</option>
<option value="fr" @if(Settings::get('default_language') === 'fr')selected @endif>Fran&ccedil;ais</option>
<option value="it" @if(Settings::get('default_language') === 'it')selected @endif>Italiano</option>

View file

@ -64,7 +64,7 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">{{ Settings::get('company') }}</a>
<a class="navbar-brand" href="/">{{ Settings::get('company', 'Pterodactyl Panel') }}</a>
</div>
<div class="navbar-collapse collapse navbar-responsive-collapse">
@section('navbar-links')

View file

@ -164,7 +164,7 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">{{ Settings::get('company') }}</a>
<a class="navbar-brand" href="/">{{ Settings::get('company', 'Pterodactyl Panel') }}</a>
</div>
<div class="navbar-collapse collapse navbar-responsive-collapse">
@section('server-name')