Merge pull request #29 from Pterodactyl/add-restful-api

Add initial API Implementation
This commit is contained in:
Dane Everitt 2016-01-16 00:32:06 -05:00
commit 7670cf1466
47 changed files with 1522 additions and 462 deletions

View file

@ -22,3 +22,7 @@ MAIL_PORT=2525
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_ENCRYPTION=null MAIL_ENCRYPTION=null
API_PREFIX=api
API_VERSION=v1
API_DEBUG=false

View file

@ -55,7 +55,7 @@ class Handler extends ExceptionHandler
$e = new NotFoundHttpException($e->getMessage(), $e); $e = new NotFoundHttpException($e->getMessage(), $e);
} }
if ($request->isXmlHttpRequest() || $request->ajax() || $request->is('api/*') || $request->is('remote/*')) { if ($request->isXmlHttpRequest() || $request->ajax() || $request->is('remote/*')) {
$exception = 'An exception occured while attempting to perform this action, please try again.'; $exception = 'An exception occured while attempting to perform this action, please try again.';

View file

@ -0,0 +1,11 @@
<?php
namespace Pterodactyl\Http\Controllers\API;
use Dingo\Api\Routing\Helpers;
use Illuminate\Routing\Controller;
class BaseController extends Controller
{
use Helpers;
}

View file

@ -0,0 +1,43 @@
<?php
namespace Pterodactyl\Http\Controllers\API;
use DB;
use Illuminate\Http\Request;
use Pterodactyl\Models\Location;
/**
* @Resource("Servers")
*/
class LocationController extends BaseController
{
public function __construct()
{
//
}
/**
* List All Locations
*
* Lists all locations currently on the system.
*
* @Get("/locations")
* @Versions({"v1"})
* @Response(200)
*/
public function getLocations(Request $request)
{
$locations = Location::select('locations.*', DB::raw('GROUP_CONCAT(nodes.id) as nodes'))
->join('nodes', 'locations.id', '=', 'nodes.location')
->groupBy('locations.id')
->get();
foreach($locations as &$location) {
$location->nodes = explode(',', $location->nodes);
}
return $locations;
}
}

View file

@ -0,0 +1,177 @@
<?php
namespace Pterodactyl\Http\Controllers\API;
use Illuminate\Http\Request;
use Pterodactyl\Models;
use Pterodactyl\Transformers\NodeTransformer;
use Pterodactyl\Repositories\NodeRepository;
use Pterodactyl\Exceptions\DisplayValidationException;
use Pterodactyl\Exceptions\DisplayException;
use Dingo\Api\Exception\ResourceException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
/**
* @Resource("Servers")
*/
class NodeController extends BaseController
{
public function __construct()
{
//
}
/**
* List All Nodes
*
* Lists all nodes currently on the system.
*
* @Get("/nodes/{?page}")
* @Versions({"v1"})
* @Parameters({
* @Parameter("page", type="integer", description="The page of results to view.", default=1)
* })
* @Response(200)
*/
public function getNodes(Request $request)
{
$nodes = Models\Node::paginate(50);
return $this->response->paginator($nodes, new NodeTransformer);
}
/**
* Create a New Node
*
* @Post("/nodes")
* @Versions({"v1"})
* @Transaction({
* @Request({
* 'name' => 'My API Node',
* 'location' => 1,
* 'public' => 1,
* 'fqdn' => 'daemon.wuzzle.woo',
* 'scheme' => 'https',
* 'memory' => 10240,
* 'memory_overallocate' => 100,
* 'disk' => 204800,
* 'disk_overallocate' => -1,
* 'daemonBase' => '/srv/daemon-data',
* 'daemonSFTP' => 2022,
* 'daemonListen' => 8080
* }, headers={"Authorization": "Bearer <jwt-token>"}),
* @Response(201),
* @Response(422, body={
* "message": "A validation error occured.",
* "errors": {},
* "status_code": 422
* }),
* @Response(503, body={
* "message": "There was an error while attempting to add this node to the system.",
* "status_code": 503
* })
* })
*/
public function postNode(Request $request)
{
try {
$node = new NodeRepository;
$new = $node->create($request->all());
return $this->response->created(route('api.nodes.view', [
'id' => $new
]));
} catch (DisplayValidationException $ex) {
throw new ResourceException('A validation error occured.', json_decode($ex->getMessage(), true));
} catch (DisplayException $ex) {
throw new ResourceException($ex->getMessage());
} catch (\Exception $e) {
throw new BadRequestHttpException('There was an error while attempting to add this node to the system.');
}
}
/**
* List Specific Node
*
* Lists specific fields about a server or all fields pertaining to that node.
*
* @Get("/nodes/{id}/{?fields}")
* @Versions({"v1"})
* @Parameters({
* @Parameter("id", type="integer", required=true, description="The ID of the node to get information on."),
* @Parameter("fields", type="string", required=false, description="A comma delimidated list of fields to include.")
* })
* @Response(200)
*/
public function getNode(Request $request, $id, $fields = null)
{
$query = Models\Node::where('id', $id);
if (!is_null($request->input('fields'))) {
foreach(explode(',', $request->input('fields')) as $field) {
if (!empty($field)) {
$query->addSelect($field);
}
}
}
try {
if (!$query->first()) {
throw new NotFoundHttpException('No node by that ID was found.');
}
return $query->first();
} catch (NotFoundHttpException $ex) {
throw $ex;
} catch (\Exception $ex) {
throw new BadRequestHttpException('There was an issue with the fields passed in the request.');
}
}
/**
* List Node Allocations
*
* Returns a listing of all node allocations.
*
* @Get("/nodes/{id}/allocations")
* @Versions({"v1"})
* @Parameters({
* @Parameter("id", type="integer", required=true, description="The ID of the node to get allocations for."),
* })
* @Response(200)
*/
public function getNodeAllocations(Request $request, $id)
{
$allocations = Models\Allocation::select('ip', 'port', 'assigned_to')->where('node', $id)->orderBy('ip', 'asc')->orderBy('port', 'asc')->get();
if ($allocations->count() < 1) {
throw new NotFoundHttpException('No allocations where found for the requested node.');
}
return $allocations;
}
/**
* Delete Node
*
* @Delete("/nodes/{id}")
* @Versions({"v1"})
* @Parameters({
* @Parameter("id", type="integer", required=true, description="The ID of the node."),
* })
* @Response(204)
*/
public function deleteNode(Request $request, $id)
{
try {
$node = new NodeRepository;
$node->delete($id);
return $this->response->noContent();
} catch (DisplayException $ex) {
throw new ResourceException($ex->getMessage());
} catch(\Exception $e) {
throw new ServiceUnavailableHttpException('An error occured while attempting to delete this node.');
}
}
}

View file

@ -0,0 +1,179 @@
<?php
namespace Pterodactyl\Http\Controllers\API;
use Illuminate\Http\Request;
use Pterodactyl\Models;
use Pterodactyl\Transformers\ServerTransformer;
use Pterodactyl\Repositories\ServerRepository;
use Pterodactyl\Exceptions\DisplayValidationException;
use Pterodactyl\Exceptions\DisplayException;
use Dingo\Api\Exception\ResourceException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
/**
* @Resource("Servers")
*/
class ServerController extends BaseController
{
public function __construct()
{
//
}
/**
* List All Servers
*
* Lists all servers currently on the system.
*
* @Get("/servers/{?page}")
* @Versions({"v1"})
* @Parameters({
* @Parameter("page", type="integer", description="The page of results to view.", default=1)
* })
* @Response(200)
*/
public function getServers(Request $request)
{
$servers = Models\Server::paginate(50);
return $this->response->paginator($servers, new ServerTransformer);
}
/**
* Create Server
*
* @Post("/servers")
* @Versions({"v1"})
* @Parameters({
* @Parameter("page", type="integer", description="The page of results to view.", default=1)
* })
* @Response(201)
*/
public function postServer(Request $request)
{
try {
$server = new ServerRepository;
$new = $server->create($request->all());
return $this->response->created(route('api.servers.view', [
'id' => $new
]));
} catch (DisplayValidationException $ex) {
throw new ResourceException('A validation error occured.', json_decode($ex->getMessage(), true));
} catch (DisplayException $ex) {
throw new ResourceException($ex->getMessage());
} catch (\Exception $e) {
throw new BadRequestHttpException('There was an error while attempting to add this server to the system.');
}
}
/**
* List Specific Server
*
* Lists specific fields about a server or all fields pertaining to that server.
*
* @Get("/servers/{id}{?fields}")
* @Versions({"v1"})
* @Parameters({
* @Parameter("id", type="integer", required=true, description="The ID of the server to get information on."),
* @Parameter("fields", type="string", required=false, description="A comma delimidated list of fields to include.")
* })
* @Response(200)
*/
public function getServer(Request $request, $id)
{
$query = Models\Server::where('id', $id);
if (!is_null($request->input('fields'))) {
foreach(explode(',', $request->input('fields')) as $field) {
if (!empty($field)) {
$query->addSelect($field);
}
}
}
try {
if (!$query->first()) {
throw new NotFoundHttpException('No server by that ID was found.');
}
return $query->first();
} catch (NotFoundHttpException $ex) {
throw $ex;
} catch (\Exception $ex) {
throw new BadRequestHttpException('There was an issue with the fields passed in the request.');
}
}
/**
* Suspend Server
*
* @Post("/servers/{id}/suspend")
* @Versions({"v1"})
* @Parameters({
* @Parameter("id", type="integer", required=true, description="The ID of the server."),
* })
* @Response(204)
*/
public function postServerSuspend(Request $request, $id)
{
try {
$server = new ServerRepository;
$server->suspend($id);
} catch (DisplayException $ex) {
throw new ResourceException($ex->getMessage());
} catch (\Exception $ex) {
throw new ServiceUnavailableHttpException('An error occured while attempting to suspend this server instance.');
}
}
/**
* Unsuspend Server
*
* @Post("/servers/{id}/unsuspend")
* @Versions({"v1"})
* @Parameters({
* @Parameter("id", type="integer", required=true, description="The ID of the server."),
* })
* @Response(204)
*/
public function postServerUnsuspend(Request $request, $id)
{
try {
$server = new ServerRepository;
$server->unsuspend($id);
} catch (DisplayException $ex) {
throw new ResourceException($ex->getMessage());
} catch (\Exception $ex) {
throw new ServiceUnavailableHttpException('An error occured while attempting to unsuspend this server instance.');
}
}
/**
* Delete Server
*
* @Delete("/servers/{id}/{force}")
* @Versions({"v1"})
* @Parameters({
* @Parameter("id", type="integer", required=true, description="The ID of the server."),
* @Parameter("force", type="string", required=false, description="Use 'force' if the server should be removed regardless of daemon response."),
* })
* @Response(204)
*/
public function deleteServer(Request $request, $id, $force = null)
{
try {
$server = new ServerRepository;
$server->deleteServer($id, $force);
return $this->response->noContent();
} catch (DisplayException $ex) {
throw new ResourceException($ex->getMessage());
} catch(\Exception $e) {
throw new ServiceUnavailableHttpException('An error occured while attempting to delete this server.');
}
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Pterodactyl\Http\Controllers\API;
use Illuminate\Http\Request;
use Pterodactyl\Models;
use Pterodactyl\Transformers\ServiceTransformer;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* @Resource("Services")
*/
class ServiceController extends BaseController
{
public function __construct()
{
//
}
public function getServices(Request $request)
{
return Models\Service::all();
}
public function getService(Request $request, $id)
{
$service = Models\Service::find($id);
if (!$service) {
throw new NotFoundHttpException('No service by that ID was found.');
}
$options = Models\ServiceOptions::select('id', 'name', 'description', 'tag', 'docker_image')->where('parent_service', $service->id)->get();
foreach($options as &$opt) {
$opt->variables = Models\ServiceVariables::where('option_id', $opt->id)->get();
}
return [
'service' => $service,
'options' => $options
];
}
}

View file

@ -2,82 +2,180 @@
namespace Pterodactyl\Http\Controllers\API; namespace Pterodactyl\Http\Controllers\API;
use Gate;
use Log;
use Debugbar;
use Pterodactyl\Models\API;
use Pterodactyl\Models\User;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class UserController extends Controller use Dingo\Api\Exception\ResourceException;
use Pterodactyl\Models;
use Pterodactyl\Transformers\UserTransformer;
use Pterodactyl\Repositories\UserRepository;
use Pterodactyl\Exceptions\DisplayValidationException;
use Pterodactyl\Exceptions\DisplayException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
/**
* @Resource("Users")
*/
class UserController extends BaseController
{ {
/** /**
* Constructor * List All Users
*
* Lists all users currently on the system.
*
* @Get("/users/{?page}")
* @Versions({"v1"})
* @Parameters({
* @Parameter("page", type="integer", description="The page of results to view.", default=1)
* })
* @Response(200)
*/ */
public function __construct() public function getUsers(Request $request)
{ {
// $users = Models\User::paginate(50);
} return $this->response->paginator($users, new UserTransformer);
public function getAllUsers(Request $request)
{
// Policies don't work if the user isn't logged in for whatever reason in Laravel...
if(!API::checkPermission($request->header('X-Authorization'), 'get-users')) {
return API::noPermissionError();
}
return response()->json([
'users' => User::all()
]);
} }
/** /**
* Returns JSON response about a user given their ID. * List Specific User
* If fields are provided only those fields are returned.
* *
* Does not return protected fields (i.e. password & totp_secret) * Lists specific fields about a user or all fields pertaining to that user.
* *
* @param Request $request * @Get("/users/{id}/{fields}")
* @param int $id * @Versions({"v1"})
* @param string $fields * @Parameters({
* @return Response * @Parameter("id", type="integer", required=true, description="The ID of the user to get information on."),
* @Parameter("fields", type="string", required=false, description="A comma delimidated list of fields to include.")
* })
* @Response(200)
*/ */
public function getUser(Request $request, $id, $fields = null) public function getUser(Request $request, $id)
{ {
$query = Models\User::where('id', $id);
// Policies don't work if the user isn't logged in for whatever reason in Laravel... if (!is_null($request->input('fields'))) {
if(!API::checkPermission($request->header('X-Authorization'), 'get-users')) { foreach(explode(',', $request->input('fields')) as $field) {
return API::noPermissionError(); if (!empty($field)) {
$query->addSelect($field);
} }
if (is_null($fields)) {
return response()->json(User::find($id));
}
$query = User::where('id', $id);
$explode = explode(',', $fields);
foreach($explode as &$exploded) {
if(!empty($exploded)) {
$query->addSelect($exploded);
} }
} }
try { try {
return response()->json($query->get()); if (!$query->first()) {
} catch (\Exception $e) { throw new NotFoundHttpException('No user by that ID was found.');
if ($e instanceof \Illuminate\Database\QueryException) {
return response()->json([
'error' => 'One of the fields provided in your argument list is invalid.'
], 500);
} }
throw $e; return $query->first();
} catch (NotFoundHttpException $ex) {
throw $ex;
} catch (\Exception $ex) {
throw new BadRequestHttpException('There was an issue with the fields passed in the request.');
} }
} }
/**
* Create a New User
*
* @Post("/users")
* @Versions({"v1"})
* @Transaction({
* @Request({
* "email": "foo@example.com",
* "password": "foopassword",
* "admin": false
* }, headers={"Authorization": "Bearer <jwt-token>"}),
* @Response(201),
* @Response(422, body={
* "message": "A validation error occured.",
* "errors": {
* "email": {"The email field is required."},
* "password": {"The password field is required."},
* "admin": {"The admin field is required."}
* },
* "status_code": 422
* })
* })
*/
public function postUser(Request $request)
{
try {
$user = new UserRepository;
$create = $user->create($request->input('email'), $request->input('password'), $request->input('admin'));
return $this->response->created(route('api.users.view', [
'id' => $create
]));
} catch (DisplayValidationException $ex) {
throw new ResourceException('A validation error occured.', json_decode($ex->getMessage(), true));
} catch (DisplayException $ex) {
throw new ResourceException($ex->getMessage());
} catch (\Exception $ex) {
throw new ServiceUnavailableHttpException('Unable to create a user on the system due to an error.');
}
}
/**
* Update an Existing User
*
* The data sent in the request will be used to update the existing user on the system.
*
* @Patch("/users/{id}")
* @Versions({"v1"})
* @Transaction({
* @Request({
* "email": "new@email.com"
* }, headers={"Authorization": "Bearer <jwt-token>"}),
* @Response(200, body={"email": "new@email.com"}),
* @Response(422)
* })
* @Parameters({
* @Parameter("id", type="integer", required=true, description="The ID of the user to modify.")
* })
*/
public function patchUser(Request $request, $id)
{
try {
$user = new UserRepository;
$user->update($id, $request->all());
return Models\User::findOrFail($id);
} catch (DisplayValidationException $ex) {
throw new ResourceException('A validation error occured.', json_decode($ex->getMessage(), true));
} catch (DisplayException $ex) {
throw new ResourceException($ex->getMessage());
} catch (\Exception $ex) {
throw new ServiceUnavailableHttpException('Unable to update a user on the system due to an error.');
}
}
/**
* Delete a User
*
* @Delete("/users/{id}")
* @Versions({"v1"})
* @Transaction({
* @Request(headers={"Authorization": "Bearer <jwt-token>"}),
* @Response(204),
* @Response(422)
* })
* @Parameters({
* @Parameter("id", type="integer", required=true, description="The ID of the user to delete.")
* })
*/
public function deleteUser(Request $request, $id)
{
try {
$user = new UserRepository;
$user->delete($id);
return $this->response->noContent();
} catch (DisplayException $ex) {
throw new ResourceException($ex->getMessage());
} catch (\Exception $ex) {
throw new ServiceUnavailableHttpException('Unable to delete this user due to an error.');
}
}
} }

View file

@ -62,27 +62,18 @@ class AccountsController extends Controller
public function postNew(Request $request) public function postNew(Request $request)
{ {
$this->validate($request, [
'email' => 'required|email|unique:users,email',
'password' => 'required|confirmed|regex:((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})'
]);
try { try {
$user = new UserRepository; $user = new UserRepository;
$userid = $user->create($request->input('username'), $request->input('email'), $request->input('password')); $userid = $user->create($request->input('username'), $request->input('email'), $request->input('password'));
if (!$userid) {
throw new \Exception('Unable to create user, response was not an integer.');
}
Alert::success('Account has been successfully created.')->flash(); Alert::success('Account has been successfully created.')->flash();
return redirect()->route('admin.accounts.view', ['id' => $userid]); return redirect()->route('admin.accounts.view', ['id' => $userid]);
} catch (\Exception $e) { } catch (\Pterodactyl\Exceptions\DisplayValidationException $ex) {
Log::error($e); return redirect()->route('admin.nodes.view', $id)->withErrors(json_decode($e->getMessage()))->withInput();
} catch (\Exception $ex) {
Log::error($ex);
Alert::danger('An error occured while attempting to add a new user. ' . $e->getMessage())->flash(); Alert::danger('An error occured while attempting to add a new user. ' . $e->getMessage())->flash();
return redirect()->route('admin.accounts.new'); return redirect()->route('admin.accounts.new');
} }
} }
public function postUpdate(Request $request) public function postUpdate(Request $request)

View file

@ -17,7 +17,6 @@ class Kernel extends HttpKernel
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class, \Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Pterodactyl\Http\Middleware\VerifyCsrfToken::class,
]; ];
/** /**
@ -30,7 +29,7 @@ class Kernel extends HttpKernel
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'guest' => \Pterodactyl\Http\Middleware\RedirectIfAuthenticated::class, 'guest' => \Pterodactyl\Http\Middleware\RedirectIfAuthenticated::class,
'server' => \Pterodactyl\Http\Middleware\CheckServer::class, 'server' => \Pterodactyl\Http\Middleware\CheckServer::class,
'api' => \Pterodactyl\Http\Middleware\APIAuthenticate::class,
'admin' => \Pterodactyl\Http\Middleware\AdminAuthenticate::class, 'admin' => \Pterodactyl\Http\Middleware\AdminAuthenticate::class,
'csrf' => \Pterodactyl\Http\Middleware\VerifyCsrfToken::class,
]; ];
} }

View file

@ -1,46 +0,0 @@
<?php
namespace Pterodactyl\Http\Middleware;
use Closure;
use Debugbar;
use Pterodactyl\Models\API;
class APIAuthenticate
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if(!$request->header('X-Authorization')) {
return response()->json([
'error' => 'Authorization header was missing with this request. Please pass the \'X-Authorization\' header with your request.'
], 403);
}
$api = API::where('key', $request->header('X-Authorization'))->first();
if (!$api) {
return response()->json([
'error' => 'Invalid API key was provided in the request.'
], 403);
}
if (!is_null($api->allowed_ips)) {
if (!in_array($request->ip(), json_decode($api->allowed_ips, true))) {
return response()->json([
'error' => 'This IP (' . $request->ip() . ') is not permitted to access the API with that token.'
], 403);
}
}
return $next($request);
}
}

View file

@ -0,0 +1,80 @@
<?php
namespace Pterodactyl\Http\Middleware;
use Pterodactyl\Models\APIKey;
use Pterodactyl\Models\APIPermission;
use Illuminate\Http\Request;
use Dingo\Api\Routing\Route;
use Dingo\Api\Auth\Provider\Authorization;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; // 400
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; // 401
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; // 403
class APISecretToken extends Authorization
{
protected $algo = 'sha256';
protected $permissionAllowed = false;
public function __construct()
{
//
}
public function getAuthorizationMethod()
{
return 'Authorization';
}
public function authenticate(Request $request, Route $route)
{
if (!$request->bearerToken() || empty($request->bearerToken())) {
throw new UnauthorizedHttpException('The authentication header was missing or malformed');
}
list($public, $hashed) = explode('.', $request->bearerToken());
$key = APIKey::where('public', $public)->first();
if (!$key) {
throw new AccessDeniedHttpException('Invalid API Key.');
}
// Check for Resource Permissions
if (!empty($request->route()->getName())) {
if(!is_null($key->allowed_ips)) {
if (!in_array($request->ip(), json_decode($key->allowed_ips))) {
throw new AccessDeniedHttpException('This IP address does not have permission to use this API key.');
}
}
foreach(APIPermission::where('key_id', $key->id)->get() as &$row) {
if ($row->permission === '*' || $row->permission === $request->route()->getName()) {
$this->permissionAllowed = true;
continue;
}
}
if (!$this->permissionAllowed) {
throw new AccessDeniedHttpException('You do not have permission to access this resource.');
}
}
if($this->_generateHMAC($request->fullUrl(), $request->getContent(), $key->secret) !== base64_decode($hashed)) {
throw new BadRequestHttpException('The hashed body was not valid. Potential modification of contents in route.');
}
return true;
}
protected function _generateHMAC($url, $body, $key)
{
$data = urldecode($url) . '.' . $body;
return hash_hmac($this->algo, $data, $key, true);
}
}

View file

@ -0,0 +1,129 @@
<?php
namespace Pterodactyl\Http\Routes;
use Pterodactyl\Models;
use Illuminate\Routing\Router;
class APIRoutes
{
public function map(Router $router) {
$api = app('Dingo\Api\Routing\Router');
$api->version('v1', ['middleware' => 'api.auth'], function ($api) {
/**
* User Routes
*/
$api->get('users', [
'as' => 'api.users',
'uses' => 'Pterodactyl\Http\Controllers\API\UserController@getUsers'
]);
$api->post('users', [
'as' => 'api.users.post',
'uses' => 'Pterodactyl\Http\Controllers\API\UserController@postUser'
]);
$api->get('users/{id}', [
'as' => 'api.users.view',
'uses' => 'Pterodactyl\Http\Controllers\API\UserController@getUser'
]);
$api->patch('users/{id}', [
'as' => 'api.users.patch',
'uses' => 'Pterodactyl\Http\Controllers\API\UserController@patchUser'
]);
$api->delete('users/{id}', [
'as' => 'api.users.delete',
'uses' => 'Pterodactyl\Http\Controllers\API\UserController@deleteUser'
]);
/**
* Server Routes
*/
$api->get('servers', [
'as' => 'api.servers',
'uses' => 'Pterodactyl\Http\Controllers\API\ServerController@getServers'
]);
$api->post('servers', [
'as' => 'api.servers.post',
'uses' => 'Pterodactyl\Http\Controllers\API\ServerController@postServer'
]);
$api->get('servers/{id}', [
'as' => 'api.servers.view',
'uses' => 'Pterodactyl\Http\Controllers\API\ServerController@getServer'
]);
$api->post('servers/{id}/suspend', [
'as' => 'api.servers.suspend',
'uses' => 'Pterodactyl\Http\Controllers\API\ServerController@postServerSuspend'
]);
$api->post('servers/{id}/unsuspend', [
'as' => 'api.servers.unsuspend',
'uses' => 'Pterodactyl\Http\Controllers\API\ServerController@postServerUnsuspend'
]);
$api->delete('servers/{id}/{force?}', [
'as' => 'api.servers.delete',
'uses' => 'Pterodactyl\Http\Controllers\API\ServerController@deleteServer'
]);
/**
* Node Routes
*/
$api->get('nodes', [
'as' => 'api.nodes',
'uses' => 'Pterodactyl\Http\Controllers\API\NodeController@getNodes'
]);
$api->post('nodes', [
'as' => 'api.nodes.post',
'uses' => 'Pterodactyl\Http\Controllers\API\NodeController@postNode'
]);
$api->get('nodes/{id}', [
'as' => 'api.nodes.view',
'uses' => 'Pterodactyl\Http\Controllers\API\NodeController@getNode'
]);
$api->get('nodes/{id}/allocations', [
'as' => 'api.nodes.view_allocations',
'uses' => 'Pterodactyl\Http\Controllers\API\NodeController@getNodeAllocations'
]);
$api->delete('nodes/{id}', [
'as' => 'api.nodes.view',
'uses' => 'Pterodactyl\Http\Controllers\API\NodeController@deleteNode'
]);
/**
* Location Routes
*/
$api->get('locations', [
'as' => 'api.locations',
'uses' => 'Pterodactyl\Http\Controllers\API\LocationController@getLocations'
]);
/**
* Service Routes
*/
$api->get('services', [
'as' => 'api.services',
'uses' => 'Pterodactyl\Http\Controllers\API\ServiceController@getServices'
]);
$api->get('services/{id}', [
'as' => 'api.services.view',
'uses' => 'Pterodactyl\Http\Controllers\API\ServiceController@getService'
]);
});
}
}

View file

@ -13,7 +13,8 @@ class AdminRoutes {
'as' => 'admin.index', 'as' => 'admin.index',
'middleware' => [ 'middleware' => [
'auth', 'auth',
'admin' 'admin',
'csrf'
], ],
'uses' => 'Admin\BaseController@getIndex' 'uses' => 'Admin\BaseController@getIndex'
]); ]);
@ -22,7 +23,8 @@ class AdminRoutes {
'prefix' => 'admin/accounts', 'prefix' => 'admin/accounts',
'middleware' => [ 'middleware' => [
'auth', 'auth',
'admin' 'admin',
'csrf'
] ]
], function () use ($router) { ], function () use ($router) {
@ -66,7 +68,8 @@ class AdminRoutes {
'prefix' => 'admin/servers', 'prefix' => 'admin/servers',
'middleware' => [ 'middleware' => [
'auth', 'auth',
'admin' 'admin',
'csrf'
] ]
], function () use ($router) { ], function () use ($router) {
@ -148,7 +151,8 @@ class AdminRoutes {
'prefix' => 'admin/nodes', 'prefix' => 'admin/nodes',
'middleware' => [ 'middleware' => [
'auth', 'auth',
'admin' 'admin',
'csrf'
] ]
], function () use ($router) { ], function () use ($router) {
@ -204,7 +208,8 @@ class AdminRoutes {
'prefix' => 'admin/locations', 'prefix' => 'admin/locations',
'middleware' => [ 'middleware' => [
'auth', 'auth',
'admin' 'admin',
'csrf'
] ]
], function () use ($router) { ], function () use ($router) {
$router->get('/', [ $router->get('/', [

View file

@ -12,7 +12,8 @@ class AuthRoutes {
$router->group([ $router->group([
'prefix' => 'auth', 'prefix' => 'auth',
'middleware' => [ 'middleware' => [
'guest' 'guest',
'csrf'
] ]
], function () use ($router) { ], function () use ($router) {

View file

@ -31,7 +31,8 @@ class BaseRoutes {
$router->group([ $router->group([
'profix' => 'account', 'profix' => 'account',
'middleware' => [ 'middleware' => [
'auth' 'auth',
'csrf'
] ]
], function () use ($router) { ], function () use ($router) {
$router->get('account', [ $router->get('account', [
@ -50,7 +51,8 @@ class BaseRoutes {
$router->group([ $router->group([
'prefix' => 'account/totp', 'prefix' => 'account/totp',
'middleware' => [ 'middleware' => [
'auth' 'auth',
'csrf'
] ]
], function () use ($router) { ], function () use ($router) {
$router->get('/', [ $router->get('/', [

View file

@ -1,31 +0,0 @@
<?php
namespace Pterodactyl\Http\Routes;
use Illuminate\Routing\Router;
class RestRoutes {
public function map(Router $router) {
$router->group([
'prefix' => 'api/v1',
'middleware' => [
'api'
]
], function () use ($router) {
// Users endpoint for API
$router->group(['prefix' => 'users'], function () use ($router) {
// Returns all users
$router->get('/', [
'uses' => 'API\UserController@getAllUsers'
]);
// Return listing of user [with only specified fields]
$router->get('/{id}/{fields?}', [
'uses' => 'API\UserController@getUser'
])->where('id', '[0-9]+');
});
});
}
}

View file

@ -11,7 +11,8 @@ class ServerRoutes {
'prefix' => 'server/{server}', 'prefix' => 'server/{server}',
'middleware' => [ 'middleware' => [
'auth', 'auth',
'server' 'server',
'csrf'
] ]
], function ($server) use ($router) { ], function ($server) use ($router) {
// Index View for Server // Index View for Server

View file

@ -1,63 +0,0 @@
<?php
namespace Pterodactyl\Models;
use Log;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Models\APIPermission;
use Illuminate\Database\Eloquent\Model;
class API extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'api';
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = ['daemonSecret'];
public function permissions()
{
return $this->hasMany(APIPermission::class);
}
public static function findKey($key)
{
return self::where('key', $key)->first();
}
/**
* Determine if an API key has permission to perform an action.
*
* @param string $key
* @param string $permission
* @return boolean
*/
public static function checkPermission($key, $permission)
{
$api = self::findKey($key);
if (!$api) {
throw new DisplayException('The requested API key (' . $key . ') was not found in the system.');
}
return APIPermission::check($api->id, $permission);
}
public static function noPermissionError($error = 'You do not have permission to perform this action with this API key.')
{
return response()->json([
'error' => 'You do not have permission to perform this action with this API key.'
], 403);
}
}

17
app/Models/APIKey.php Normal file
View file

@ -0,0 +1,17 @@
<?php
namespace Pterodactyl\Models;
use Illuminate\Database\Eloquent\Model;
class APIKey extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'api_keys';
}

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Models; namespace Pterodactyl\Models;
use Debugbar;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class APIPermission extends Model class APIPermission extends Model
@ -15,16 +14,4 @@ class APIPermission extends Model
*/ */
protected $table = 'api_permissions'; protected $table = 'api_permissions';
/**
* Checks if an API key has a specific permission.
*
* @param int $id
* @param string $permission
* @return boolean
*/
public static function check($id, $permission)
{
return self::where('key_id', $id)->where('permission', $permission)->exists();
}
} }

View file

@ -34,7 +34,7 @@ class User extends Model implements AuthenticatableContract,
* *
* @var array * @var array
*/ */
protected $fillable = ['name', 'email', 'password']; protected $fillable = ['name', 'email', 'password', 'use_totp', 'totp_secret', 'language'];
/** /**
* The attributes excluded from the model's JSON form. * The attributes excluded from the model's JSON form.

View file

@ -183,4 +183,10 @@ class NodeRepository {
} }
} }
public function delete($id)
{
// @TODO: add logic;
return true;
}
} }

View file

@ -685,4 +685,28 @@ class ServerRepository
return $server->save(); return $server->save();
} }
/**
* Suspends a server instance making it unable to be booted or used by a user.
* @param integer $id
* @return boolean
*/
public function suspend($id)
{
// @TODO: Implement logic; not doing it now since that is outside of the
// scope of this API brance.
return true;
}
/**
* Unsuspends a server instance.
* @param integer $id
* @return boolean
*/
public function unsuspend($id)
{
// @TODO: Implement logic; not doing it now since that is outside of the
// scope of this API brance.
return true;
}
} }

View file

@ -2,11 +2,16 @@
namespace Pterodactyl\Repositories; namespace Pterodactyl\Repositories;
use DB;
use Hash; use Hash;
use Validator;
use Pterodactyl\Models\User; use Pterodactyl\Models;
use Pterodactyl\Services\UuidService; use Pterodactyl\Services\UuidService;
use Pterodactyl\Exceptions\DisplayValidationException;
use Pterodactyl\Exceptions\DisplayException;
class UserRepository class UserRepository
{ {
@ -22,32 +27,70 @@ class UserRepository
* @param string $password An unhashed version of the user's password. * @param string $password An unhashed version of the user's password.
* @return bool|integer * @return bool|integer
*/ */
public function create($email, $password) public function create($email, $password, $admin = false)
{ {
$user = new User; $validator = Validator::make([
'email' => $email,
'password' => $password,
'root_admin' => $admin
], [
'email' => 'required|email|unique:users,email',
'password' => 'required|regex:((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})',
'root_admin' => 'required|boolean'
]);
// Run validator, throw catchable and displayable exception if it fails.
// Exception includes a JSON result of failed validation rules.
if ($validator->fails()) {
throw new DisplayValidationException($validator->errors());
}
$user = new Models\User;
$uuid = new UuidService; $uuid = new UuidService;
$user->uuid = $uuid->generate('users', 'uuid'); $user->uuid = $uuid->generate('users', 'uuid');
$user->email = $email; $user->email = $email;
$user->password = Hash::make($password); $user->password = Hash::make($password);
$user->language = 'en';
$user->root_admin = ($admin) ? 1 : 0;
return ($user->save()) ? $user->id : false; try {
$user->save();
return $user->id;
} catch (\Exception $ex) {
throw $e;
}
} }
/** /**
* Updates a user on the panel. * Updates a user on the panel.
* *
* @param integer $id * @param integer $id
* @param array $user An array of columns and their associated values to update for the user. * @param array $data An array of columns and their associated values to update for the user.
* @return boolean * @return boolean
*/ */
public function update($id, array $user) public function update($id, array $data)
{ {
if(array_key_exists('password', $user)) { $validator = Validator::make($data, [
$user['password'] = Hash::make($user['password']); 'email' => 'email|unique:users,email,' . $id,
'password' => 'regex:((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})',
'root_admin' => 'boolean',
'language' => 'string|min:1|max:5',
'use_totp' => 'boolean',
'totp_secret' => 'size:16'
]);
// Run validator, throw catchable and displayable exception if it fails.
// Exception includes a JSON result of failed validation rules.
if ($validator->fails()) {
throw new DisplayValidationException($validator->errors());
} }
return User::find($id)->update($user); if(array_key_exists('password', $data)) {
$user['password'] = Hash::make($data['password']);
}
return Models\User::findOrFail($id)->update($data);
} }
/** /**
@ -58,7 +101,22 @@ class UserRepository
*/ */
public function delete($id) public function delete($id)
{ {
return User::destroy($id); if(Models\Server::where('owner', $id)->count() > 0) {
throw new DisplayException('Cannot delete a user with active servers attached to thier account.');
}
DB::beginTransaction();
Models\Permission::where('user_id', $id)->delete();
Models\Subuser::where('user_id', $id)->delete();
Models\User::destroy($id);
try {
DB::commit();
return true;
} catch (\Exception $ex) {
throw $ex;
}
} }
} }

View file

@ -0,0 +1,21 @@
<?php
namespace Pterodactyl\Transformers;
use Pterodactyl\Models\Node;
use League\Fractal\TransformerAbstract;
class NodeTransformer extends TransformerAbstract
{
/**
* Turn this item object into a generic array
*
* @return array
*/
public function transform(Node $node)
{
return $node;
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Pterodactyl\Transformers;
use Pterodactyl\Models\Server;
use League\Fractal\TransformerAbstract;
class ServerTransformer extends TransformerAbstract
{
/**
* Turn this item object into a generic array
*
* @return array
*/
public function transform(Server $server)
{
return $server;
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Pterodactyl\Transformers;
use Pterodactyl\Models\User;
use League\Fractal\TransformerAbstract;
class UserTransformer extends TransformerAbstract
{
/**
* Turn this item object into a generic array
*
* @return array
*/
public function transform(User $user)
{
return $user;
}
}

View file

@ -8,6 +8,7 @@
"php": ">=5.5.9", "php": ">=5.5.9",
"laravel/framework": "5.2.*", "laravel/framework": "5.2.*",
"barryvdh/laravel-debugbar": "^2.0", "barryvdh/laravel-debugbar": "^2.0",
"dingo/api": "1.0.*@dev",
"doctrine/dbal": "^2.5", "doctrine/dbal": "^2.5",
"guzzlehttp/guzzle": "^6.1", "guzzlehttp/guzzle": "^6.1",
"pragmarx/google2fa": "^0.7.1", "pragmarx/google2fa": "^0.7.1",

209
config/api.php Normal file
View file

@ -0,0 +1,209 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Standards Tree
|--------------------------------------------------------------------------
|
| Versioning an API with Dingo revolves around content negotiation and
| custom MIME types. A custom type will belong to one of three
| standards trees, the Vendor tree (vnd), the Personal tree
| (prs), and the Unregistered tree (x).
|
| By default the Unregistered tree (x) is used, however, should you wish
| to you can register your type with the IANA. For more details:
| https://tools.ietf.org/html/rfc6838
|
*/
'standardsTree' => env('API_STANDARDS_TREE', 'x'),
/*
|--------------------------------------------------------------------------
| API Subtype
|--------------------------------------------------------------------------
|
| Your subtype will follow the standards tree you use when used in the
| "Accept" header to negotiate the content type and version.
|
| For example: Accept: application/x.SUBTYPE.v1+json
|
*/
'subtype' => env('API_SUBTYPE', 'pterodactyl'),
/*
|--------------------------------------------------------------------------
| Default API Version
|--------------------------------------------------------------------------
|
| This is the default version when strict mode is disabled and your API
| is accessed via a web browser. It's also used as the default version
| when generating your APIs documentation.
|
*/
'version' => env('API_VERSION', 'v1'),
/*
|--------------------------------------------------------------------------
| Default API Prefix
|--------------------------------------------------------------------------
|
| A default prefix to use for your API routes so you don't have to
| specify it for each group.
|
*/
'prefix' => env('API_PREFIX', null),
/*
|--------------------------------------------------------------------------
| Default API Domain
|--------------------------------------------------------------------------
|
| A default domain to use for your API routes so you don't have to
| specify it for each group.
|
*/
'domain' => env('API_DOMAIN', null),
/*
|--------------------------------------------------------------------------
| Name
|--------------------------------------------------------------------------
|
| When documenting your API using the API Blueprint syntax you can
| configure a default name to avoid having to manually specify
| one when using the command.
|
*/
'name' => env('API_NAME', 'Pterodactyl Panel API'),
/*
|--------------------------------------------------------------------------
| Conditional Requests
|--------------------------------------------------------------------------
|
| Globally enable conditional requests so that an ETag header is added to
| any successful response. Subsequent requests will perform a check and
| will return a 304 Not Modified. This can also be enabled or disabled
| on certain groups or routes.
|
*/
'conditionalRequest' => env('API_CONDITIONAL_REQUEST', true),
/*
|--------------------------------------------------------------------------
| Strict Mode
|--------------------------------------------------------------------------
|
| Enabling strict mode will require clients to send a valid Accept header
| with every request. This also voids the default API version, meaning
| your API will not be browsable via a web browser.
|
*/
'strict' => env('API_STRICT', false),
/*
|--------------------------------------------------------------------------
| Debug Mode
|--------------------------------------------------------------------------
|
| Enabling debug mode will result in error responses caused by thrown
| exceptions to have a "debug" key that will be populated with
| more detailed information on the exception.
|
*/
'debug' => env('API_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Generic Error Format
|--------------------------------------------------------------------------
|
| When some HTTP exceptions are not caught and dealt with the API will
| generate a generic error response in the format provided. Any
| keys that aren't replaced with corresponding values will be
| removed from the final response.
|
*/
'errorFormat' => [
'message' => ':message',
'errors' => ':errors',
'code' => ':code',
'status_code' => ':status_code',
'debug' => ':debug',
],
/*
|--------------------------------------------------------------------------
| Authentication Providers
|--------------------------------------------------------------------------
|
| The authentication providers that should be used when attempting to
| authenticate an incoming API request.
|
*/
'auth' => [
'custom' => 'Pterodactyl\Http\Middleware\APISecretToken'
],
/*
|--------------------------------------------------------------------------
| Throttling / Rate Limiting
|--------------------------------------------------------------------------
|
| Consumers of your API can be limited to the amount of requests they can
| make. You can create your own throttles or simply change the default
| throttles.
|
*/
'throttling' => [
],
/*
|--------------------------------------------------------------------------
| Response Transformer
|--------------------------------------------------------------------------
|
| Responses can be transformed so that they are easier to format. By
| default a Fractal transformer will be used to transform any
| responses prior to formatting. You can easily replace
| this with your own transformer.
|
*/
'transformer' => env('API_TRANSFORMER', Dingo\Api\Transformer\Adapter\Fractal::class),
/*
|--------------------------------------------------------------------------
| Response Formats
|--------------------------------------------------------------------------
|
| Responses can be returned in multiple formats by registering different
| response formatters. You can also customize an existing response
| formatter.
|
*/
'defaultFormat' => env('API_DEFAULT_FORMAT', 'json'),
'formats' => [
'json' => Dingo\Api\Http\Response\Format\Json::class,
],
];

View file

@ -112,6 +112,8 @@ return [
'providers' => [ 'providers' => [
Dingo\Api\Provider\LaravelServiceProvider::class,
/* /*
* Laravel Framework Service Providers... * Laravel Framework Service Providers...
*/ */
@ -179,6 +181,8 @@ return [
'Crypt' => Illuminate\Support\Facades\Crypt::class, 'Crypt' => Illuminate\Support\Facades\Crypt::class,
'DB' => Illuminate\Support\Facades\DB::class, 'DB' => Illuminate\Support\Facades\DB::class,
'Debugbar' => Barryvdh\Debugbar\Facade::class, 'Debugbar' => Barryvdh\Debugbar\Facade::class,
'DingoAPI' => Dingo\Api\Facade\API::class,
'DingoRoute' => Dingo\Api\Facade\Route::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class, 'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class, 'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class, 'File' => Illuminate\Support\Facades\File::class,

View file

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateTableApiKeys extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('api_keys', function (Blueprint $table) {
$table->increments('id');
$table->char('public', 16);
$table->char('secret', 32);
$table->json('allowed_ips')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('api_keys');
}
}

View file

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateTableApiPermissions extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('api_permissions', function (Blueprint $table) {
$table->increments('id');
$table->integer('key_id')->unsigned();
$table->string('permission');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('api_permissions');
}
}