From 70db4610751c939c6fb576044332dd5ef88afb61 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 10 Mar 2017 18:25:12 -0500 Subject: [PATCH] Start push of service changes. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes the way service files are stored and allows for much easier updates in the future that won’t affect custom services. Also stores more configurations in the database to make life easier for everyone. --- .../Controllers/Admin/OptionController.php | 71 +++ .../Controllers/Admin/ServiceController.php | 477 ++++++++++-------- .../Controllers/Daemon/ServiceController.php | 62 ++- app/Http/Routes/AdminRoutes.php | 143 +++--- app/Repositories/OptionRepository.php | 80 +++ .../Pack.php => PackRepository.php} | 4 +- app/Repositories/ServiceRepository.php | 178 +++++++ app/Repositories/ServiceRepository/Option.php | 124 ----- .../ServiceRepository/Service.php | 144 ------ .../Variable.php => VariableRepository.php} | 4 +- config/pterodactyl.php | 18 + ...5_212803_DeleteServiceExecutableOption.php | 38 ++ ..._10_162934_AddNewServiceOptionsColumns.php | 52 ++ ...03_10_173607_MigrateToNewServiceSystem.php | 223 ++++++++ .../admin/services/index.blade.php | 67 +++ .../pterodactyl/admin/services/new.blade.php | 86 ++++ .../admin/services/options/view.blade.php | 162 ++++++ .../pterodactyl/admin/services/view.blade.php | 113 +++++ .../pterodactyl/layouts/admin.blade.php | 11 + 19 files changed, 1459 insertions(+), 598 deletions(-) create mode 100644 app/Http/Controllers/Admin/OptionController.php create mode 100644 app/Repositories/OptionRepository.php rename app/Repositories/{ServiceRepository/Pack.php => PackRepository.php} (99%) create mode 100644 app/Repositories/ServiceRepository.php delete mode 100644 app/Repositories/ServiceRepository/Option.php delete mode 100644 app/Repositories/ServiceRepository/Service.php rename app/Repositories/{ServiceRepository/Variable.php => VariableRepository.php} (98%) create mode 100644 config/pterodactyl.php create mode 100644 database/migrations/2017_03_05_212803_DeleteServiceExecutableOption.php create mode 100644 database/migrations/2017_03_10_162934_AddNewServiceOptionsColumns.php create mode 100644 database/migrations/2017_03_10_173607_MigrateToNewServiceSystem.php create mode 100644 resources/themes/pterodactyl/admin/services/index.blade.php create mode 100644 resources/themes/pterodactyl/admin/services/new.blade.php create mode 100644 resources/themes/pterodactyl/admin/services/options/view.blade.php create mode 100644 resources/themes/pterodactyl/admin/services/view.blade.php diff --git a/app/Http/Controllers/Admin/OptionController.php b/app/Http/Controllers/Admin/OptionController.php new file mode 100644 index 000000000..3dc9b24ff --- /dev/null +++ b/app/Http/Controllers/Admin/OptionController.php @@ -0,0 +1,71 @@ +. + * + * 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\Admin; + +use Log; +use Alert; +use Storage; +use Pterodactyl\Models; +use Illuminate\Http\Request; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Repositories\OptionRepository; +use Pterodactyl\Exceptions\DisplayValidationException; + +class OptionController extends Controller +{ + /** + * Display option overview page. + * + * @param Request $request + * @param int $id + * @return \Illuminate\View\View + */ + public function viewConfiguration(Request $request, $id) + { + return view('admin.services.options.view', ['option' => Models\ServiceOption::findOrFail($id)]); + } + + public function editConfiguration(Request $request, $id) + { + $repo = new OptionRepository; + + try { + $repo->update($id, $request->intersect([ + 'name', 'description', 'tag', 'docker_image', 'startup', + 'config_from', 'config_stop', 'config_logs', 'config_files', 'config_startup', + ])); + + Alert::success('Service option configuration has been successfully updated.')->flash(); + } catch (DisplayValidationException $ex) { + return redirect()->route('admin.services.option.view', $id)->withErrors(json_decode($ex->getMessage())); + } catch (\Exception $ex) { + Log::error($ex); + Alert::danger('An unhandled exception occurred while attempting to update this service option. This error has been logged.')->flash(); + } + + return redirect()->route('admin.services.option.view', $id); + } +} diff --git a/app/Http/Controllers/Admin/ServiceController.php b/app/Http/Controllers/Admin/ServiceController.php index e831c8683..81ca528b8 100644 --- a/app/Http/Controllers/Admin/ServiceController.php +++ b/app/Http/Controllers/Admin/ServiceController.php @@ -36,276 +36,311 @@ use Pterodactyl\Exceptions\DisplayValidationException; class ServiceController extends Controller { - public function __construct() - { - // - } - - public function getIndex(Request $request) + /** + * Display service overview page. + * + * @param Request $request + * @return \Illuminate\View\View + */ + public function index(Request $request) { return view('admin.services.index', [ - 'services' => Models\Service::withCount('servers')->get(), + 'services' => Models\Service::withCount('servers', 'options', 'packs')->get(), ]); } - public function getNew(Request $request) + /** + * Display create service page. + * + * @param Request $request + * @return \Illuminate\View\View + */ + public function new(Request $request) { return view('admin.services.new'); } - public function postNew(Request $request) + /** + * Return base view for a service. + * + * @param Request $request + * @param int $id + * @return \Illuminate\View\View + */ + public function view(Request $request, $id) { + return view('admin.services.view', [ + 'service' => Models\Service::with('options', 'options.servers')->findOrFail($id), + ]); + } + + /** + * Handle post action for new service. + * + * @param Request $request + * @return \Illuminate\Response\RedirectResponse + */ + public function create(Request $request) + { + $repo = new ServiceRepository; + try { - $repo = new ServiceRepository\Service; - $service = $repo->create($request->only([ - 'name', 'description', 'file', - 'executable', 'startup', + $service = $repo->create($request->intersect([ + 'name', 'description', 'folder', 'startup', ])); Alert::success('Successfully created new service!')->flash(); - return redirect()->route('admin.services.service', $service->id); + return redirect()->route('admin.services.view', $service->id); } catch (DisplayValidationException $ex) { return redirect()->route('admin.services.new')->withErrors(json_decode($ex->getMessage()))->withInput(); } catch (DisplayException $ex) { Alert::danger($ex->getMessage())->flash(); } catch (\Exception $ex) { Log::error($ex); - Alert::danger('An error occured while attempting to add a new service.')->flash(); + Alert::danger('An error occured while attempting to add a new service. This error has been logged.')->flash(); } return redirect()->route('admin.services.new')->withInput(); } - public function getService(Request $request, $service) + /** + * Delete a service from the system. + * + * @param Request $request + * @param int $id + * @return \Illuminate\Response\RedirectResponse + */ + public function delete(Request $request, $id) { - return view('admin.services.view', [ - 'service' => Models\Service::with('options', 'options.servers')->findOrFail($service), - ]); - } + $repo = new ServiceRepository; - public function postService(Request $request, $service) - { try { - $repo = new ServiceRepository\Service; - $repo->update($service, $request->only([ - 'name', 'description', 'file', - 'executable', 'startup', - ])); - Alert::success('Successfully updated this service.')->flash(); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.services.service', $service)->withErrors(json_decode($ex->getMessage()))->withInput(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An error occurred while attempting to update this service.')->flash(); - } - - return redirect()->route('admin.services.service', $service)->withInput(); - } - - public function deleteService(Request $request, $service) - { - try { - $repo = new ServiceRepository\Service; - $repo->delete($service); - Alert::success('Successfully deleted that service.')->flash(); + $repo->delete($id); + Alert::success('Successfully deleted service.')->flash(); return redirect()->route('admin.services'); } catch (DisplayException $ex) { Alert::danger($ex->getMessage())->flash(); } catch (\Exception $ex) { Log::error($ex); - Alert::danger('An error was encountered while attempting to delete that service.')->flash(); + Alert::danger('An error was encountered while attempting to delete that service. This error has been logged')->flash(); } - return redirect()->route('admin.services.service', $service); + return redirect()->route('admin.services.view', $id); } - public function getOption(Request $request, $service, $option) + /** + * Edits configuration for a specific service. + * + * @param Request $request + * @param int $id + * @return \Illuminate\Response\RedirectResponse + */ + public function edit(Request $request, $id) { - $option = Models\ServiceOption::with('service', 'variables')->findOrFail($option); - $option->setRelation('servers', $option->servers()->with('user')->paginate(25)); + $repo = new ServiceRepository; - return view('admin.services.options.view', ['option' => $option]); - } - - public function postOption(Request $request, $service, $option) - { try { - $repo = new ServiceRepository\Option; - $repo->update($option, $request->only([ - 'name', 'description', 'tag', - 'executable', 'docker_image', 'startup', + $repo->update($id, $request->intersect([ + 'name', 'description', 'folder', 'startup', ])); - Alert::success('Option settings successfully updated.')->flash(); + Alert::success('Service has been updated successfully.')->flash(); } catch (DisplayValidationException $ex) { - return redirect()->route('admin.services.option', [$service, $option])->withErrors(json_decode($ex->getMessage()))->withInput(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An error occured while attempting to modify this option.')->flash(); - } - - return redirect()->route('admin.services.option', [$service, $option])->withInput(); - } - - public function deleteOption(Request $request, $service, $option) - { - try { - $repo = new ServiceRepository\Option; - $repo->delete($option); - - Alert::success('Successfully deleted that option.')->flash(); - - return redirect()->route('admin.services.service', $service); + return redirect()->route('admin.services.view', $id)->withErrors(json_decode($ex->getMessage()))->withInput(); } catch (DisplayException $ex) { Alert::danger($ex->getMessage())->flash(); } catch (\Exception $ex) { Log::error($ex); - Alert::danger('An error was encountered while attempting to delete this option.')->flash(); + Alert::danger('An error occurred while attempting to update this service. This error has been logged.')->flash(); } - return redirect()->route('admin.services.option', [$service, $option]); + return redirect()->route('admin.services.view', $id); } - public function postOptionVariable(Request $request, $service, $option, $variable) - { - try { - $repo = new ServiceRepository\Variable; - - // Because of the way old() works on the display side we prefix all of the variables with thier ID - // We need to remove that prefix here since the repo doesn't want it. - $data = [ - 'user_viewable' => '0', - 'user_editable' => '0', - 'required' => '0', - ]; - foreach ($request->except(['_token']) as $id => $val) { - $data[str_replace($variable . '_', '', $id)] = $val; - } - $repo->update($variable, $data); - Alert::success('Successfully updated variable.')->flash(); - } catch (DisplayValidationException $ex) { - $data = []; - foreach (json_decode($ex->getMessage(), true) as $id => $val) { - $data[$variable . '_' . $id] = $val; - } - - return redirect()->route('admin.services.option', [$service, $option])->withErrors((object) $data)->withInput(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An error occurred while attempting to update this service.')->flash(); - } - - return redirect()->route('admin.services.option', [$service, $option])->withInput(); - } - - public function getNewVariable(Request $request, $service, $option) - { - return view('admin.services.options.variable', [ - 'option' => Models\ServiceOption::with('service')->findOrFail($option), - ]); - } - - public function postNewVariable(Request $request, $service, $option) - { - try { - $repo = new ServiceRepository\Variable; - $repo->create($option, $request->only([ - 'name', 'description', 'env_variable', - 'default_value', 'user_viewable', - 'user_editable', 'required', 'regex', - ])); - Alert::success('Successfully added new variable to this option.')->flash(); - - return redirect()->route('admin.services.option', [$service, $option]); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.services.option.variable.new', [$service, $option])->withErrors(json_decode($ex->getMessage()))->withInput(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An error occurred while attempting to add this variable.')->flash(); - } - - return redirect()->route('admin.services.option.variable.new', [$service, $option])->withInput(); - } - - public function newOption(Request $request, $service) - { - return view('admin.services.options.new', [ - 'service' => Models\Service::findOrFail($service), - ]); - } - - public function postNewOption(Request $request, $service) - { - try { - $repo = new ServiceRepository\Option; - $id = $repo->create($service, $request->except([ - '_token', - ])); - Alert::success('Successfully created new service option.')->flash(); - - return redirect()->route('admin.services.option', [$service, $id]); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.services.option.new', $service)->withErrors(json_decode($ex->getMessage()))->withInput(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An error occured while attempting to add this service option.')->flash(); - } - - return redirect()->route('admin.services.option.new', $service)->withInput(); - } - - public function deleteVariable(Request $request, $service, $option, $variable) - { - try { - $repo = new ServiceRepository\Variable; - $repo->delete($variable); - Alert::success('Deleted variable.')->flash(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An error occured while attempting to delete that variable.')->flash(); - } - - return redirect()->route('admin.services.option', [$service, $option]); - } - - public function getConfiguration(Request $request, $serviceId) - { - $service = Models\Service::findOrFail($serviceId); - - return view('admin.services.config', [ - 'service' => $service, - 'contents' => [ - 'json' => Storage::get('services/' . $service->file . '/main.json'), - 'index' => Storage::get('services/' . $service->file . '/index.js'), - ], - ]); - } - - public function postConfiguration(Request $request, $serviceId) - { - try { - $repo = new ServiceRepository\Service; - $repo->updateFile($serviceId, $request->only(['file', 'contents'])); - - return response('', 204); - } catch (DisplayException $ex) { - return response()->json([ - 'error' => $ex->getMessage(), - ], 503); - } catch (\Exception $ex) { - Log::error($ex); - - return response()->json([ - 'error' => 'An error occured while attempting to save the file.', - ], 503); - } - } + // public function getOption(Request $request, $service, $option) + // { + // $option = Models\ServiceOption::with('service', 'variables')->findOrFail($option); + // $option->setRelation('servers', $option->servers()->with('user')->paginate(25)); + // + // return view('admin.services.options.view', ['option' => $option]); + // } + // + // public function postOption(Request $request, $service, $option) + // { + // try { + // $repo = new ServiceRepository\Option; + // $repo->update($option, $request->only([ + // 'name', 'description', 'tag', + // 'executable', 'docker_image', 'startup', + // ])); + // Alert::success('Option settings successfully updated.')->flash(); + // } catch (DisplayValidationException $ex) { + // return redirect()->route('admin.services.option', [$service, $option])->withErrors(json_decode($ex->getMessage()))->withInput(); + // } catch (\Exception $ex) { + // Log::error($ex); + // Alert::danger('An error occured while attempting to modify this option.')->flash(); + // } + // + // return redirect()->route('admin.services.option', [$service, $option])->withInput(); + // } + // + // public function deleteOption(Request $request, $service, $option) + // { + // try { + // $repo = new ServiceRepository\Option; + // $repo->delete($option); + // + // Alert::success('Successfully deleted that option.')->flash(); + // + // return redirect()->route('admin.services.service', $service); + // } catch (DisplayException $ex) { + // Alert::danger($ex->getMessage())->flash(); + // } catch (\Exception $ex) { + // Log::error($ex); + // Alert::danger('An error was encountered while attempting to delete this option.')->flash(); + // } + // + // return redirect()->route('admin.services.option', [$service, $option]); + // } + // + // public function postOptionVariable(Request $request, $service, $option, $variable) + // { + // try { + // $repo = new ServiceRepository\Variable; + // + // // Because of the way old() works on the display side we prefix all of the variables with thier ID + // // We need to remove that prefix here since the repo doesn't want it. + // $data = [ + // 'user_viewable' => '0', + // 'user_editable' => '0', + // 'required' => '0', + // ]; + // foreach ($request->except(['_token']) as $id => $val) { + // $data[str_replace($variable . '_', '', $id)] = $val; + // } + // $repo->update($variable, $data); + // Alert::success('Successfully updated variable.')->flash(); + // } catch (DisplayValidationException $ex) { + // $data = []; + // foreach (json_decode($ex->getMessage(), true) as $id => $val) { + // $data[$variable . '_' . $id] = $val; + // } + // + // return redirect()->route('admin.services.option', [$service, $option])->withErrors((object) $data)->withInput(); + // } catch (DisplayException $ex) { + // Alert::danger($ex->getMessage())->flash(); + // } catch (\Exception $ex) { + // Log::error($ex); + // Alert::danger('An error occurred while attempting to update this service.')->flash(); + // } + // + // return redirect()->route('admin.services.option', [$service, $option])->withInput(); + // } + // + // public function getNewVariable(Request $request, $service, $option) + // { + // return view('admin.services.options.variable', [ + // 'option' => Models\ServiceOption::with('service')->findOrFail($option), + // ]); + // } + // + // public function postNewVariable(Request $request, $service, $option) + // { + // try { + // $repo = new ServiceRepository\Variable; + // $repo->create($option, $request->only([ + // 'name', 'description', 'env_variable', + // 'default_value', 'user_viewable', + // 'user_editable', 'required', 'regex', + // ])); + // Alert::success('Successfully added new variable to this option.')->flash(); + // + // return redirect()->route('admin.services.option', [$service, $option]); + // } catch (DisplayValidationException $ex) { + // return redirect()->route('admin.services.option.variable.new', [$service, $option])->withErrors(json_decode($ex->getMessage()))->withInput(); + // } catch (DisplayException $ex) { + // Alert::danger($ex->getMessage())->flash(); + // } catch (\Exception $ex) { + // Log::error($ex); + // Alert::danger('An error occurred while attempting to add this variable.')->flash(); + // } + // + // return redirect()->route('admin.services.option.variable.new', [$service, $option])->withInput(); + // } + // + // public function newOption(Request $request, $service) + // { + // return view('admin.services.options.new', [ + // 'service' => Models\Service::findOrFail($service), + // ]); + // } + // + // public function postNewOption(Request $request, $service) + // { + // try { + // $repo = new ServiceRepository\Option; + // $id = $repo->create($service, $request->except([ + // '_token', + // ])); + // Alert::success('Successfully created new service option.')->flash(); + // + // return redirect()->route('admin.services.option', [$service, $id]); + // } catch (DisplayValidationException $ex) { + // return redirect()->route('admin.services.option.new', $service)->withErrors(json_decode($ex->getMessage()))->withInput(); + // } catch (\Exception $ex) { + // Log::error($ex); + // Alert::danger('An error occured while attempting to add this service option.')->flash(); + // } + // + // return redirect()->route('admin.services.option.new', $service)->withInput(); + // } + // + // public function deleteVariable(Request $request, $service, $option, $variable) + // { + // try { + // $repo = new ServiceRepository\Variable; + // $repo->delete($variable); + // Alert::success('Deleted variable.')->flash(); + // } catch (DisplayException $ex) { + // Alert::danger($ex->getMessage())->flash(); + // } catch (\Exception $ex) { + // Log::error($ex); + // Alert::danger('An error occured while attempting to delete that variable.')->flash(); + // } + // + // return redirect()->route('admin.services.option', [$service, $option]); + // } + // + // public function getConfiguration(Request $request, $serviceId) + // { + // $service = Models\Service::findOrFail($serviceId); + // + // return view('admin.services.config', [ + // 'service' => $service, + // 'contents' => [ + // 'json' => Storage::get('services/' . $service->file . '/main.json'), + // 'index' => Storage::get('services/' . $service->file . '/index.js'), + // ], + // ]); + // } + // + // public function postConfiguration(Request $request, $serviceId) + // { + // try { + // $repo = new ServiceRepository\Service; + // $repo->updateFile($serviceId, $request->only(['file', 'contents'])); + // + // return response('', 204); + // } catch (DisplayException $ex) { + // return response()->json([ + // 'error' => $ex->getMessage(), + // ], 503); + // } catch (\Exception $ex) { + // Log::error($ex); + // + // return response()->json([ + // 'error' => 'An error occured while attempting to save the file.', + // ], 503); + // } + // } } diff --git a/app/Http/Controllers/Daemon/ServiceController.php b/app/Http/Controllers/Daemon/ServiceController.php index e362f0d06..fbbad9b4e 100644 --- a/app/Http/Controllers/Daemon/ServiceController.php +++ b/app/Http/Controllers/Daemon/ServiceController.php @@ -25,35 +25,28 @@ namespace Pterodactyl\Http\Controllers\Daemon; use Storage; -use Pterodactyl\Models; use Illuminate\Http\Request; +use Pterodactyl\Models\Service; +use Pterodactyl\Models\ServiceOption; use Pterodactyl\Http\Controllers\Controller; class ServiceController extends Controller { - /** - * Controller Constructor. - */ - public function __construct() - { - // - } - /** * Returns a listing of all services currently on the system, * as well as the associated files and the file hashes for * caching purposes. * * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response + * @return \Illuminate\Http\JsonResponse */ public function list(Request $request) { $response = []; - foreach (Models\Service::all() as &$service) { - $response[$service->file] = [ - 'main.json' => sha1_file(storage_path('app/services/' . $service->file . '/main.json')), - 'index.js' => sha1_file(storage_path('app/services/' . $service->file . '/index.js')), + foreach (Service::all() as $service) { + $response[$service->folder] = [ + 'main.json' => sha1($this->getConfiguration($service->id)->toJson()), + 'index.js' => sha1_file(storage_path('app/services/' . $service->folder . '/index.js')), ]; } @@ -64,16 +57,45 @@ class ServiceController extends Controller * Returns the contents of the requested file for the given service. * * @param \Illuminate\Http\Request $request - * @param string $service + * @param string $folder * @param string $file - * @return \Illuminate\Http\Response + * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\FileResponse */ - public function pull(Request $request, $service, $file) + public function pull(Request $request, $folder, $file) { - if (! Storage::exists('services/' . $service . '/' . $file)) { - return response()->json(['error' => 'No such file.'], 404); + $service = Service::where('folder', $folder)->firstOrFail(); + + if ($file === 'index.js') { + return response()->file(storage_path('app/services/' . $service->folder . '/index.js')); + } else if ($file === 'main.json') { + return response()->json($this->getConfiguration($service->id)); } - return response()->file(storage_path('app/services/' . $service . '/' . $file)); + return abort(404); + } + + /** + * Returns a `main.json` file based on the configuration + * of each service option. + * + * @param int $id + * @return \Illuminate\Support\Collection + */ + protected function getConfiguration($id) + { + $options = ServiceOption::where('service_id', $id)->get(); + + return $options->mapWithKeys(function ($item) use ($options) { + return [ + $item->tag => array_filter([ + 'symlink' => $options->where('id', $item->config_from)->pluck('tag')->pop(), + 'startup' => json_decode($item->config_startup), + 'stop' => $item->config_stop, + 'configs' => json_decode($item->config_files), + 'log' => json_decode($item->config_logs), + 'query' => 'none', + ]), + ]; + }); } } diff --git a/app/Http/Routes/AdminRoutes.php b/app/Http/Routes/AdminRoutes.php index c25cb91e3..edf8fdc91 100644 --- a/app/Http/Routes/AdminRoutes.php +++ b/app/Http/Routes/AdminRoutes.php @@ -386,128 +386,101 @@ class AdminRoutes ], function () use ($router) { $router->get('/', [ 'as' => 'admin.services', - 'uses' => 'Admin\ServiceController@getIndex', + 'uses' => 'Admin\ServiceController@index', ]); $router->get('/new', [ 'as' => 'admin.services.new', - 'uses' => 'Admin\ServiceController@getNew', + 'uses' => 'Admin\ServiceController@new', ]); $router->post('/new', [ - 'uses' => 'Admin\ServiceController@postNew', + 'uses' => 'Admin\ServiceController@create', ]); - $router->get('/service/{id}', [ - 'as' => 'admin.services.service', - 'uses' => 'Admin\ServiceController@getService', + $router->get('/view/{id}', [ + 'as' => 'admin.services.view', + 'uses' => 'Admin\ServiceController@view', ]); - $router->post('/service/{id}', [ - 'uses' => 'Admin\ServiceController@postService', + $router->post('/view/{id}', [ + 'uses' => 'Admin\ServiceController@edit', ]); - $router->delete('/service/{id}', [ - 'uses' => 'Admin\ServiceController@deleteService', + $router->delete('/view/{id}', [ + 'uses' => 'Admin\ServiceController@delete', ]); - $router->get('/service/{id}/configuration', [ - 'as' => 'admin.services.service.config', - 'uses' => 'Admin\ServiceController@getConfiguration', - ]); - - $router->post('/service/{id}/configuration', [ - 'uses' => 'Admin\ServiceController@postConfiguration', - ]); - - $router->get('/service/{service}/option/new', [ + // --------------------- + // Service Option Routes + // --------------------- + $router->get('/option/new', [ 'as' => 'admin.services.option.new', - 'uses' => 'Admin\ServiceController@newOption', + 'uses' => 'Admin\OptionController@new', ]); - $router->post('/service/{service}/option/new', [ - 'uses' => 'Admin\ServiceController@postNewOption', + $router->get('/option/{id}', [ + 'as' => 'admin.services.option.view', + 'uses' => 'Admin\OptionController@viewConfiguration', ]); - $router->get('/service/{service}/option/{option}', [ - 'as' => 'admin.services.option', - 'uses' => 'Admin\ServiceController@getOption', + $router->get('/option/{id}/variables', [ + 'as' => 'admin.services.option.view.variables', + 'uses' => 'Admin\OptionController@viewVariables', ]); - $router->post('/service/{service}/option/{option}', [ - 'uses' => 'Admin\ServiceController@postOption', + $router->post('/option/{id}', [ + 'uses' => 'Admin\OptionController@editConfiguration', ]); - $router->delete('/service/{service}/option/{id}', [ - 'uses' => 'Admin\ServiceController@deleteOption', - ]); - - $router->get('/service/{service}/option/{option}/variable/new', [ - 'as' => 'admin.services.option.variable.new', - 'uses' => 'Admin\ServiceController@getNewVariable', - ]); - - $router->post('/service/{service}/option/{option}/variable/new', [ - 'uses' => 'Admin\ServiceController@postNewVariable', - ]); - - $router->post('/service/{service}/option/{option}/variable/{variable}', [ - 'as' => 'admin.services.option.variable', - 'uses' => 'Admin\ServiceController@postOptionVariable', - ]); - - $router->get('/service/{service}/option/{option}/variable/{variable}/delete', [ - 'as' => 'admin.services.option.variable.delete', - 'uses' => 'Admin\ServiceController@deleteVariable', - ]); }); // Service Packs $router->group([ - 'prefix' => 'admin/services/packs', + 'prefix' => 'admin/packs', 'middleware' => [ 'auth', 'admin', 'csrf', ], ], function () use ($router) { - $router->get('/new/{option?}', [ - 'as' => 'admin.services.packs.new', - 'uses' => 'Admin\PackController@new', - ]); - $router->post('/new', [ - 'uses' => 'Admin\PackController@create', - ]); - $router->get('/upload/{option?}', [ - 'as' => 'admin.services.packs.uploadForm', - 'uses' => 'Admin\PackController@uploadForm', - ]); - $router->post('/upload', [ - 'uses' => 'Admin\PackController@postUpload', - ]); + // $router->get('/new/{option?}', [ + // 'as' => 'admin.packs.new', + // 'uses' => 'Admin\PackController@new', + // ]); + // $router->post('/new', [ + // 'uses' => 'Admin\PackController@create', + // ]); + // $router->get('/upload/{option?}', [ + // 'as' => 'admin.packs.uploadForm', + // 'uses' => 'Admin\PackController@uploadForm', + // ]); + // $router->post('/upload', [ + // 'uses' => 'Admin\PackController@postUpload', + // ]); $router->get('/', [ - 'as' => 'admin.services.packs', + 'as' => 'admin.packs', 'uses' => 'Admin\PackController@listAll', ]); - $router->get('/for/option/{option}', [ - 'as' => 'admin.services.packs.option', - 'uses' => 'Admin\PackController@listByOption', - ]); - $router->get('/for/service/{service}', [ - 'as' => 'admin.services.packs.service', - 'uses' => 'Admin\PackController@listByService', - ]); - $router->get('/edit/{pack}', [ - 'as' => 'admin.services.packs.edit', - 'uses' => 'Admin\PackController@edit', - ]); - $router->post('/edit/{pack}', [ - 'uses' => 'Admin\PackController@update', - ]); - $router->get('/edit/{pack}/export/{archive?}', [ - 'as' => 'admin.services.packs.export', - 'uses' => 'Admin\PackController@export', - ]); + // $router->get('/for/option/{option}', [ + // 'as' => 'admin.packs.option', + // 'uses' => 'Admin\PackController@listByOption', + // ]); + // $router->get('/for/service/{service}', [ + // 'as' => 'admin.packs.service', + // 'uses' => 'Admin\PackController@listByService', + // ]); + // $router->get('/edit/{pack}', [ + // 'as' => 'admin.packs.edit', + // 'uses' => 'Admin\PackController@edit', + // ]); + // $router->post('/edit/{pack}', [ + // 'uses' => 'Admin\PackController@update', + // ]); + // $router->get('/edit/{pack}/export/{archive?}', [ + // 'as' => 'admin.packs.export', + // 'uses' => 'Admin\PackController@export', + // ]); }); } } diff --git a/app/Repositories/OptionRepository.php b/app/Repositories/OptionRepository.php new file mode 100644 index 000000000..7ded09689 --- /dev/null +++ b/app/Repositories/OptionRepository.php @@ -0,0 +1,80 @@ +. + * + * 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\Repositories; + +use DB; +use Validator; +use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Exceptions\DisplayValidationException; + +class OptionRepository +{ + /** + * Updates a service option in the database which can then be used + * on nodes. + * + * @param int $id + * @param array $data + * @return \Pterodactyl\Models\ServiceOption + */ + public function update($id, array $data) + { + $option = ServiceOption::findOrFail($id); + + $validator = Validator::make($data, [ + 'name' => 'sometimes|required|string|max:255', + 'description' => 'sometimes|required|string', + 'tag' => 'sometimes|required|string|max:255|unique:service_options,tag,' . $option->id, + 'docker_image' => 'sometimes|required|string|max:255', + 'startup' => 'sometimes|required|string', + 'config_from' => 'sometimes|required|numeric|exists:service_options,id', + ]); + + $validator->sometimes('config_startup', 'required_without:config_from|json', function ($input) use ($option) { + return ! (! $input->config_from && ! is_null($option->config_from)); + }); + + $validator->sometimes('config_stop', 'required_without:config_from|string|max:255', function ($input) use ($option) { + return ! (! $input->config_from && ! is_null($option->config_from)); + }); + + $validator->sometimes('config_logs', 'required_without:config_from|json', function ($input) use ($option) { + return ! (! $input->config_from && ! is_null($option->config_from)); + }); + + $validator->sometimes('config_files', 'required_without:config_from|json', function ($input) use ($option) { + return ! (! $input->config_from && ! is_null($option->config_from)); + }); + + if ($validator->fails()) { + throw new DisplayValidationException($validator->errors()); + } + + $option->fill($data)->save(); + + return $option; + } +} diff --git a/app/Repositories/ServiceRepository/Pack.php b/app/Repositories/PackRepository.php similarity index 99% rename from app/Repositories/ServiceRepository/Pack.php rename to app/Repositories/PackRepository.php index 2132c4954..842bfdac6 100644 --- a/app/Repositories/ServiceRepository/Pack.php +++ b/app/Repositories/PackRepository.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Repositories\ServiceRepository; +namespace Pterodactyl\Repositories; use DB; use Uuid; @@ -33,7 +33,7 @@ use Pterodactyl\Services\UuidService; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\DisplayValidationException; -class Pack +class PackRepository { public function __construct() { diff --git a/app/Repositories/ServiceRepository.php b/app/Repositories/ServiceRepository.php new file mode 100644 index 000000000..ed9dbf03d --- /dev/null +++ b/app/Repositories/ServiceRepository.php @@ -0,0 +1,178 @@ +. + * + * 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\Repositories; + +use DB; +use Uuid; +use Storage; +use Validator; +use Pterodactyl\Models\Service; +use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Exceptions\DisplayValidationException; + +class ServiceRepository +{ + /** + * Creates a new service on the system. + * + * @param array $data + * @return \Pterodactyl\Models\Service + */ + public function create(array $data) + { + $validator = Validator::make($data, [ + 'name' => 'required|string|min:1|max:255', + 'description' => 'required|nullable|string', + 'folder' => 'required|unique:services,folder|regex:/^[\w.-]{1,50}$/', + 'startup' => 'required|nullable|string', + ]); + + if ($validator->fails()) { + throw new DisplayValidationException($validator->errors()); + } + + $service = DB::transaction(function () use ($data) { + $service = Service::create([ + 'author' => config('pterodactyl.service.author'), + 'name' => $data['name'], + 'description' => (isset($data['description'])) ? $data['description'] : null, + 'folder' => $data['folder'], + 'startup' => (isset($data['startup'])) ? $data['startup'] : null, + ]); + + // It is possible for an event to return false or throw an exception + // which won't necessarily be detected by this transaction. + // + // This check ensures the model was actually saved. + if (! $service->exists) { + throw new \Exception('Service model was created however the response appears to be invalid. Did an event fire wrongly?'); + } + + Storage::copy('services/.templates/index.js', 'services/' . $service->folder . '/index.js'); + + return $service; + }); + + return $service; + } + + /** + * Updates a service. + * + * @param int $id + * @param array $data + * @return \Pterodactyl\Models\Service + */ + public function update($id, array $data) + { + $service = Service::findOrFail($id); + + $validator = Validator::make($data, [ + 'name' => 'sometimes|required|string|min:1|max:255', + 'description' => 'sometimes|required|nullable|string', + 'folder' => 'sometimes|required|regex:/^[\w.-]{1,50}$/', + 'startup' => 'sometimes|required|nullable|string', + ]); + + if ($validator->fails()) { + throw new DisplayValidationException($validator->errors()); + } + + return DB::transaction(function () use ($data, $service) { + $moveFiles = (isset($data['folder']) && $data['folder'] !== $service->folder); + $oldFolder = $service->folder; + + $service->fill($data); + $service->save(); + + if ($moveFiles) { + Storage::move(sprintf('services/%s/index.js', $oldFolder), sprintf('services/%s/index.js', $service->folder)); + } + + return $service; + }); + } + + /** + * Deletes a service and associated files and options. + * + * @param int $id + * @return void + */ + public function delete($id) + { + $service = Service::withCount('servers', 'options')->findOrFail($id); + + if ($service->servers_count > 0) { + throw new DisplayException('You cannot delete a service that has servers associated with it.'); + } + + DB::transaction(function () use ($service) { + ServiceVariable::whereIn('option_id', $service->options->pluck('id')->all())->delete(); + + $service->options->each(function ($item) { + $item->delete(); + }); + + $service->delete(); + Storage::deleteDirectory('services/' . $service->folder); + }); + } + + /** + * Updates a service file on the system. + * + * @param int $id + * @param array $data + * @return void + * + * @deprecated + */ + // public function updateFile($id, array $data) + // { + // $service = Service::findOrFail($id); + // + // $validator = Validator::make($data, [ + // 'file' => 'required|in:index', + // 'contents' => 'required|string', + // ]); + // + // if ($validator->fails()) { + // throw new DisplayValidationException($validator->errors()); + // } + // + // $filepath = 'services/' . $service->folder . '/' . $filename; + // $backup = 'services/.bak/' . str_random(12) . '.bak'; + // + // try { + // Storage::move($filepath, $backup); + // Storage::put($filepath, $data['contents']); + // } catch (\Exception $ex) { + // Storage::move($backup, $filepath); + // throw $ex; + // } + // } +} diff --git a/app/Repositories/ServiceRepository/Option.php b/app/Repositories/ServiceRepository/Option.php deleted file mode 100644 index e0b82d347..000000000 --- a/app/Repositories/ServiceRepository/Option.php +++ /dev/null @@ -1,124 +0,0 @@ -. - * - * 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\Repositories\ServiceRepository; - -use DB; -use Validator; -use Pterodactyl\Models; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class Option -{ - public function __construct() - { - // - } - - public function create($service, array $data) - { - $service = Models\Service::findOrFail($service); - - $validator = Validator::make($data, [ - 'name' => 'required|string|max:255', - 'description' => 'required|string|min:1', - 'tag' => 'required|string|max:255', - 'executable' => 'sometimes|string|max:255', - 'docker_image' => 'required|string|max:255', - 'startup' => 'sometimes|string', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException($validator->errors()); - } - - if (isset($data['executable']) && empty($data['executable'])) { - $data['executable'] = null; - } - - if (isset($data['startup']) && empty($data['startup'])) { - $data['startup'] = null; - } - - $option = new Models\ServiceOption; - $option->service_id = $service->id; - $option->fill($data); - $option->save(); - - return $option->id; - } - - public function delete($id) - { - $option = Models\ServiceOption::findOrFail($id); - $servers = Models\Server::where('option', $option->id)->get(); - - if (count($servers) !== 0) { - throw new DisplayException('You cannot delete an option that has servers attached to it currently.'); - } - - DB::beginTransaction(); - - try { - Models\ServiceVariable::where('option_id', $option->id)->delete(); - $option->delete(); - - DB::commit(); - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - } - - public function update($id, array $data) - { - $option = Models\ServiceOption::findOrFail($id); - - $validator = Validator::make($data, [ - 'name' => 'sometimes|required|string|max:255', - 'description' => 'sometimes|required|string|min:1', - 'tag' => 'sometimes|required|string|max:255', - 'executable' => 'sometimes|string|max:255', - 'docker_image' => 'sometimes|required|string|max:255', - 'startup' => 'sometimes|string', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException($validator->errors()); - } - - if (isset($data['executable']) && empty($data['executable'])) { - $data['executable'] = null; - } - - if (isset($data['startup']) && empty($data['startup'])) { - $data['startup'] = null; - } - - $option->fill($data); - - return $option->save(); - } -} diff --git a/app/Repositories/ServiceRepository/Service.php b/app/Repositories/ServiceRepository/Service.php deleted file mode 100644 index 206479a0a..000000000 --- a/app/Repositories/ServiceRepository/Service.php +++ /dev/null @@ -1,144 +0,0 @@ -. - * - * 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\Repositories\ServiceRepository; - -use DB; -use Uuid; -use Storage; -use Validator; -use Pterodactyl\Models; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class Service -{ - public function __construct() - { - // - } - - public function create(array $data) - { - $validator = Validator::make($data, [ - 'name' => 'required|string|min:1|max:255', - 'description' => 'required|string', - 'file' => 'required|unique:services,file|regex:/^[\w.-]{1,50}$/', - 'executable' => 'max:255|regex:/^(.*)$/', - 'startup' => 'string', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException($validator->errors()); - } - - DB::beginTransaction(); - try { - $service = new Models\Service; - $service->author = env('SERVICE_AUTHOR', (string) Uuid::generate(4)); - $service->fill($data); - $service->save(); - - Storage::put('services/' . $service->file . '/main.json', '{}'); - Storage::copy('services/.templates/index.js', 'services/' . $service->file . '/index.js'); - DB::commit(); - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - - return $service; - } - - public function update($id, array $data) - { - $service = Models\Service::findOrFail($id); - - $validator = Validator::make($data, [ - 'name' => 'sometimes|required|string|min:1|max:255', - 'description' => 'sometimes|required|string', - 'file' => 'sometimes|required|regex:/^[\w.-]{1,50}$/', - 'executable' => 'sometimes|max:255|regex:/^(.*)$/', - 'startup' => 'sometimes|string', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException($validator->errors()); - } - - $service->fill($data); - - return $service->save(); - } - - public function delete($id) - { - $service = Models\Service::findOrFail($id); - $servers = Models\Server::where('service', $service->id)->get(); - $options = Models\ServiceOption::select('id')->where('service_id', $service->id); - - if (count($servers) !== 0) { - throw new DisplayException('You cannot delete a service that has servers associated with it.'); - } - - DB::beginTransaction(); - try { - Models\ServiceVariable::whereIn('option_id', $options->get()->toArray())->delete(); - $options->delete(); - $service->delete(); - - Storage::deleteDirectory('services/' . $service->file); - DB::commit(); - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - } - - public function updateFile($id, array $data) - { - $service = Models\Service::findOrFail($id); - - $validator = Validator::make($data, [ - 'file' => 'required|in:index,main', - 'contents' => 'required|string', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException($validator->errors()); - } - - $filename = ($data['file'] === 'main') ? 'main.json' : 'index.js'; - $filepath = 'services/' . $service->file . '/' . $filename; - $backup = 'services/.bak/' . str_random(12) . '.bak'; - - try { - Storage::move($filepath, $backup); - Storage::put($filepath, $data['contents']); - } catch (\Exception $ex) { - Storage::move($backup, $filepath); - throw $ex; - } - } -} diff --git a/app/Repositories/ServiceRepository/Variable.php b/app/Repositories/VariableRepository.php similarity index 98% rename from app/Repositories/ServiceRepository/Variable.php rename to app/Repositories/VariableRepository.php index f9836ff0f..744cb8bc7 100644 --- a/app/Repositories/ServiceRepository/Variable.php +++ b/app/Repositories/VariableRepository.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Repositories\ServiceRepository; +namespace Pterodactyl\Repositories; use DB; use Validator; @@ -30,7 +30,7 @@ use Pterodactyl\Models; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\DisplayValidationException; -class Variable +class VariableRepository { public function __construct() { diff --git a/config/pterodactyl.php b/config/pterodactyl.php new file mode 100644 index 000000000..ec0e430cd --- /dev/null +++ b/config/pterodactyl.php @@ -0,0 +1,18 @@ + [ + 'author' => env('SERVICE_AUTHOR'), + ], + +]; diff --git a/database/migrations/2017_03_05_212803_DeleteServiceExecutableOption.php b/database/migrations/2017_03_05_212803_DeleteServiceExecutableOption.php new file mode 100644 index 000000000..374219539 --- /dev/null +++ b/database/migrations/2017_03_05_212803_DeleteServiceExecutableOption.php @@ -0,0 +1,38 @@ +dropColumn('executable'); + $table->renameColumn('file', 'folder'); + $table->text('description')->nullable()->change(); + $table->text('startup')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('services', function (Blueprint $table) { + $table->string('executable')->after('folder'); + $table->renameColumn('folder', 'file'); + $table->text('description')->nullable(false)->change(); + $table->text('startup')->nullable(false)->change(); + }); + } +} diff --git a/database/migrations/2017_03_10_162934_AddNewServiceOptionsColumns.php b/database/migrations/2017_03_10_162934_AddNewServiceOptionsColumns.php new file mode 100644 index 000000000..535c3b477 --- /dev/null +++ b/database/migrations/2017_03_10_162934_AddNewServiceOptionsColumns.php @@ -0,0 +1,52 @@ +dropColumn('executable'); + + $table->unsignedInteger('config_from')->nullable()->after('docker_image'); + $table->string('config_stop')->nullable()->after('docker_image'); + $table->text('config_logs')->nullable()->after('docker_image'); + $table->text('config_startup')->nullable()->after('docker_image'); + $table->text('config_files')->nullable()->after('docker_image'); + + $table->foreign('config_from')->references('id')->on('service_options'); + }); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + DB::transaction(function() { + Schema::table('service_options', function (Blueprint $table) { + $table->dropForeign('config_from'); + + $table->dropColumn('config_from'); + $table->dropColumn('config_stop'); + $table->dropColumn('config_logs'); + $table->dropColumn('config_startup'); + $table->dropColumn('config_files'); + + $table->string('executable')->after('docker_image')->nullable(); + }); + }); + } +} diff --git a/database/migrations/2017_03_10_173607_MigrateToNewServiceSystem.php b/database/migrations/2017_03_10_173607_MigrateToNewServiceSystem.php new file mode 100644 index 000000000..afa9f6ee1 --- /dev/null +++ b/database/migrations/2017_03_10_173607_MigrateToNewServiceSystem.php @@ -0,0 +1,223 @@ +. + * + * 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. + */ +use Pterodactyl\Models\Service; +use Pterodactyl\Models\ServiceOption; +use Illuminate\Support\Facades\Schema; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Migrations\Migration; + +class MigrateToNewServiceSystem extends Migration +{ + protected $services; + + /** + * Run the migrations. + * + * @return void + */ + public function up() + { + $this->services = Service::where('author', 'ptrdctyl-v040-11e6-8b77-86f30ca893d3')->get(); + + $this->minecraft(); + $this->srcds(); + $this->terraria(); + $this->voice(); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // Not doing reversals right now... + } + + public function minecraft() + { + $service = $this->services->where('folder', 'minecraft')->first(); + if (! $service) { + return; + } + + // Set New Default Startup + $service->startup = 'java -Xms128M -Xmx{{SERVER_MEMORY}}M -jar {{SERVER_JARFILE}}'; + + DB::transaction(function () use ($service) { + $options = ServiceOption::where('service_id', $service->id)->get(); + $options->each(function ($item) use ($options) { + switch($item->tag) { + case 'vanilla': + $item->config_startup = '{"done": ")! For help, type ", "userInteraction": [ "Go to eula.txt for more info."]}'; + $item->config_files = '{"server.properties":{"parser": "properties", "find":{"server-ip": "0.0.0.0", "enable-query": "true", "server-port": "{{server.build.default.port}}", "query.port": "{{server.build.default.port}}"}}}'; + $item->config_logs = '{"custom": false, "location": "logs/latest.log"}'; + $item->config_stop = 'stop'; + break; + case 'spigot': + $item->startup = null; + $item->config_from = $options->where('tag', 'vanilla')->pluck('id')->pop(); + $item->config_files = '{"spigot.yml":{"parser": "yaml", "find":{"settings.restart-on-crash": "false"}}}'; + break; + case 'bungeecord': + $item->config_startup = '{"done": "Listening on ", "userInteraction": [ "Listening on /0.0.0.0:25577"]}'; + $item->config_files = '{"config.yml":{"parser": "yaml", "find":{"listeners[0].query_enabled": true, "listeners[0].query_port": "{{server.build.default.port}}", "listeners[0].host": "0.0.0.0:{{server.build.default.port}}", "servers.*.address":{"127.0.0.1": "{{config.docker.interface}}", "localhost": "{{config.docker.interface}}"}}}}'; + $item->config_logs = '{"custom": false, "location": "proxy.log.0"}'; + $item->config_stop = 'end'; + break; + case 'sponge': + $item->startup = null; + $item->config_from = $options->where('tag', 'vanilla')->pluck('id')->pop(); + $item->config_startup = '{"userInteraction": [ "You need to agree to the EULA"]}'; + break; + default: + break; + } + + $item->save(); + }); + + $service->save(); + }); + } + + public function srcds() + { + $service = $this->services->where('folder', 'srcds')->first(); + if (! $service) { + return; + } + + $service->startup = './srcds_run -game {{SRCDS_GAME}} -console -port {{SERVER_PORT}} +ip 0.0.0.0 -strictportbind -norestart'; + + DB::transaction(function () use ($service) { + $options = ServiceOption::where('service_id', $service->id)->get(); + $options->each(function ($item) use ($options) { + if ($item->tag === 'srcds' && $item->name === 'Insurgency') { + $item->tag = 'insurgency'; + } else if ($item->tag === 'srcds' && $item->name === 'Team Fortress 2') { + $item->tag = 'tf2'; + } else if ($item->tag === 'srcds' && $item->name === 'Custom Source Engine Game') { + $item->tag = 'source'; + } + + switch($item->tag) { + case 'source': + $item->config_startup = '{"done": "Assigned anonymous gameserver Steam ID", "userInteraction": []}'; + $item->config_files = '{}'; + $item->config_logs = '{"custom": true, "location": "logs/latest.log"}'; + $item->config_stop = 'quit'; + break; + case 'insurgency': + case 'tf2': + $item->startup = './srcds_run -game {{SRCDS_GAME}} -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart'; + $item->config_from = $options->where('name', 'Custom Source Engine Game')->pluck('id')->pop(); + break; + case 'ark': + $item->startup = './ShooterGame/Binaries/Linux/ShooterGameServer TheIsland?listen?ServerPassword={{ARK_PASSWORD}}?ServerAdminPassword={{ARK_ADMIN_PASSWORD}}?Port={{SERVER_PORT}}?MaxPlayers={{SERVER_MAX_PLAYERS}}'; + $item->config_from = $options->where('name', 'Custom Source Engine Game')->pluck('id')->pop(); + $item->config_startup = '{"done": "Setting breakpad minidump AppID"}'; + $item->config_stop = '^C'; + break; + default: + break; + } + + $item->save(); + }); + + $service->save(); + }); + } + + public function terraria() + { + $service = $this->services->where('folder', 'terraria')->first(); + if (! $service) { + return; + } + + $service->startup = 'mono TerrariaServer.exe -port {{SERVER_PORT}} -autocreate 2 -worldname World'; + + DB::transaction(function () use ($service) { + $options = ServiceOption::where('service_id', $service->id)->get(); + $options->each(function ($item) use ($options) { + switch($item->tag) { + case 'tshock': + $item->startup = null; + $item->config_startup = '{"done": "Type \'help\' for a list of commands", "userInteraction": []}'; + $item->config_files = '{"tshock/config.json":{"parser": "json", "find":{"ServerPort": "{{server.build.default.port}}", "MaxSlots": "{{server.build.env.MAX_SLOTS}}"}}}'; + $item->config_logs = '{"custom": false, "location": "ServerLog.txt"}'; + $item->config_stop = 'exit'; + break; + default: + break; + } + + $item->save(); + }); + + $service->save(); + }); + } + + public function voice() + { + $service = $this->services->where('folder', 'voice')->first(); + if (! $service) { + return; + } + + $service->startup = null; + + DB::transaction(function () use ($service) { + $options = ServiceOption::where('service_id', $service->id)->get(); + $options->each(function ($item) use ($options) { + switch($item->tag) { + case 'mumble': + $item->startup = './murmur.x86 -fg'; + $item->config_startup = '{"done": "Server listening on", "userInteraction": [ "Generating new server certificate"]}'; + $item->config_files = '{"murmur.ini":{"parser": "ini", "find":{"logfile": "murmur.log", "port": "{{server.build.default.port}}", "host": "0.0.0.0", "users": "{{server.build.env.MAX_USERS}}"}}}'; + $item->config_logs = '{"custom": true, "location": "logs/murmur.log"}'; + $item->config_stop = '^C'; + break; + case 'ts3': + $item->startup = './ts3server_minimal_runscript.sh default_voice_port={{SERVER_PORT}} query_port={{SERVER_PORT}}'; + $item->config_startup = '{"done": "listening on 0.0.0.0:", "userInteraction": []}'; + $item->config_files = '{"ts3server.ini":{"parser": "ini", "find":{"default_voice_port": "{{server.build.default.port}}", "voice_ip": "0.0.0.0", "query_port": "{{server.build.default.port}}", "query_ip": "0.0.0.0"}}}'; + $item->config_logs = '{"custom": true, "location": "logs/ts3.log"}'; + $item->config_stop = '^C'; + break; + default: + break; + } + + $item->save(); + }); + + $service->save(); + }); + } +} diff --git a/resources/themes/pterodactyl/admin/services/index.blade.php b/resources/themes/pterodactyl/admin/services/index.blade.php new file mode 100644 index 000000000..677af8b77 --- /dev/null +++ b/resources/themes/pterodactyl/admin/services/index.blade.php @@ -0,0 +1,67 @@ +{{-- Copyright (c) 2015 - 2017 Dane Everitt --}} + +{{-- 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. --}} +@extends('layouts.admin') + +@section('title') + Services +@endsection + +@section('content-header') +

ServicesAll services currently available on this system.

+ +@endsection + +@section('content') +
+
+
+
+

Configured Services

+
+
+ + + + + + + + + @foreach($services as $service) + + + + + + + + @endforeach +
NameDescriptionOptionsPacksServers
{{ $service->name }}{{ $service->description }}{{ $service->options_count }}{{ $service->packs_count }}{{ $service->servers_count }}
+
+ +
+
+
+@endsection diff --git a/resources/themes/pterodactyl/admin/services/new.blade.php b/resources/themes/pterodactyl/admin/services/new.blade.php new file mode 100644 index 000000000..5d215e66f --- /dev/null +++ b/resources/themes/pterodactyl/admin/services/new.blade.php @@ -0,0 +1,86 @@ +{{-- Copyright (c) 2015 - 2017 Dane Everitt --}} + +{{-- 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. --}} +@extends('layouts.admin') + +@section('title') + New Service +@endsection + +@section('content-header') +

New ServiceConfigure a new service to deploy to all nodes.

+ +@endsection + +@section('content') +
+
+
+
+
+

New Service

+
+
+
+ +
+ +

This should be a descriptive category name that emcompasses all of the options within the service.

+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+ +
+ +

Services are downloaded by the daemon and stored in a folder using this name. The storage location is /srv/daemon/services/{NAME} by default.

+
+
+
+ +
+ +

The default start command to use when running options under this service. This command can be modified per-option and should include the executable to be called in the container.

+
+
+
+ +
+
+
+
+@endsection diff --git a/resources/themes/pterodactyl/admin/services/options/view.blade.php b/resources/themes/pterodactyl/admin/services/options/view.blade.php new file mode 100644 index 000000000..01965d4ee --- /dev/null +++ b/resources/themes/pterodactyl/admin/services/options/view.blade.php @@ -0,0 +1,162 @@ +{{-- Copyright (c) 2015 - 2017 Dane Everitt --}} + +{{-- 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. --}} +@extends('layouts.admin') + +@section('title') + Services → Option: {{ $option->name }} +@endsection + +@section('content-header') +

{{ $option->name }}{{ str_limit($option->description, 50) }}

+ +@endsection + +@section('content') +
+
+ +
+
+
+
+
+
+
+

Configuration

+
+
+
+
+
+ + +

A simple, human-readable name to use as an identifier for this service.

+
+
+ + +

A description of this service that will be displayed throughout the panel as needed.

+
+
+
+
+ + +

This should be a unique identifer for this service option that is not used for any other service options.

+
+
+ + +

The default docker image that should be used for new servers under this service option. This can be left blank to use the parent service's defined image, and can also be changed per-server.

+
+
+ + +

The default statup command that should be used for new servers under this service option. This can be left blank to use the parent service's startup, and can also be changed per-server.

+
+
+
+
+
+
+
+
+
+

Process Management

+
+
+
+
+
+

The following configuration options should not be edited unless you understand how this system works. If wrongly modified it is possible for the daemon to break.

+

All fields are required unless you select a seperate option from the 'Copy Settings From' dropdown, in which case fields may be left blank to use the values from that option.

+
+
+
+
+ + +

If you would like to default to settings from another option select the option from the menu above.

+
+
+ + +

The command that should be sent to server processes to stop them gracefully. If you need to send a SIGINT you should enter ^C here.

+
+
+ + +

This should be a JSON representation of where log files are stored, and wether or not the daemon should be creating custom logs.

+
+
+
+
+ + +

This should be a JSON representation of configuration files to modify and what parts should be changed.

+
+
+ + +

This should be a JSON representation of what values the daemon should be looking for when booting a server to determine completion.

+
+
+
+ +
+
+
+ +@endsection + +@section('footer-scripts') + @parent + +@endsection diff --git a/resources/themes/pterodactyl/admin/services/view.blade.php b/resources/themes/pterodactyl/admin/services/view.blade.php new file mode 100644 index 000000000..42d151539 --- /dev/null +++ b/resources/themes/pterodactyl/admin/services/view.blade.php @@ -0,0 +1,113 @@ +{{-- Copyright (c) 2015 - 2017 Dane Everitt --}} + +{{-- 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. --}} +@extends('layouts.admin') + +@section('title') + Services: {{ $service->name }} +@endsection + +@section('content-header') +

{{ $service->name }}{{ str_limit($service->description, 50) }}

+ +@endsection + +@section('content') +
+
+
+
+
+
+ +
+ +

This should be a descriptive category name that emcompasses all of the options within the service.

+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+ +
+ +

Services are downloaded by the daemon and stored in a folder using this name. The storage location is /srv/daemon/services/{NAME} by default.

+
+
+
+ +
+ +

The default start command to use when running options under this service. This command can be modified per-option and should include the executable to be called in the container.

+
+
+
+ +
+
+
+
+
+
+
+
+

Configured Options

+
+
+ + + + + + + + @foreach($service->options as $option) + + + + + + + @endforeach +
NameDescriptionTagServers
{{ $option->name }}{!! $option->description !!}{{ $option->tag }}{{ $option->servers->count() }}
+
+ +
+
+
+@endsection diff --git a/resources/themes/pterodactyl/layouts/admin.blade.php b/resources/themes/pterodactyl/layouts/admin.blade.php index 96b8c0e98..a6c735c60 100644 --- a/resources/themes/pterodactyl/layouts/admin.blade.php +++ b/resources/themes/pterodactyl/layouts/admin.blade.php @@ -106,6 +106,17 @@ Users +
  • SERVICE MANAGEMENT
  • +
  • + + Services + +
  • +
  • + + Packs + +