First pass at converting websocket to send a token along with every call
This commit is contained in:
parent
513965dac7
commit
18c4b951e6
8 changed files with 143 additions and 135 deletions
|
@ -3,11 +3,13 @@
|
||||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||||
|
|
||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
use Illuminate\Support\Str;
|
use Lcobucci\JWT\Builder;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Lcobucci\JWT\Signer\Key;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Pterodactyl\Models\Server;
|
use Pterodactyl\Models\Server;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
||||||
use Illuminate\Contracts\Cache\Repository;
|
use Illuminate\Contracts\Cache\Repository;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||||
|
@ -32,12 +34,10 @@ class WebsocketController extends ClientApiController
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a one-time token that is sent along in the request to the Daemon. The
|
* Generates a one-time token that is sent along in every websocket call to the Daemon.
|
||||||
* daemon then connects back to the Panel to verify that the token is valid when it
|
* This is a signed JWT that the Daemon then uses the verify the user's identity, and
|
||||||
* is used.
|
* allows us to continually renew this token and avoid users mainitaining sessions wrongly,
|
||||||
*
|
* as well as ensure that user's only perform actions they're allowed to.
|
||||||
* This token is valid for 30 seconds from time of generation, it is not designed
|
|
||||||
* to be stored and used over and over.
|
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Http\Request $request
|
* @param \Illuminate\Http\Request $request
|
||||||
* @param \Pterodactyl\Models\Server $server
|
* @param \Pterodactyl\Models\Server $server
|
||||||
|
@ -51,20 +51,26 @@ class WebsocketController extends ClientApiController
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$token = Str::random(32);
|
$now = Chronos::now();
|
||||||
|
|
||||||
$this->cache->put('ws:' . $token, [
|
$signer = new Sha256;
|
||||||
'user_id' => $request->user()->id,
|
|
||||||
'server_id' => $server->id,
|
$token = (new Builder)->issuedBy(config('app.url'))
|
||||||
'request_ip' => $request->ip(),
|
->permittedFor($server->node->getConnectionAddress())
|
||||||
'timestamp' => Chronos::now()->toIso8601String(),
|
->identifiedBy(hash('sha256', $request->user()->id . $server->uuid), true)
|
||||||
], Chronos::now()->addSeconds(30));
|
->issuedAt($now->getTimestamp())
|
||||||
|
->canOnlyBeUsedAfter($now->getTimestamp())
|
||||||
|
->expiresAt($now->addMinutes(15)->getTimestamp())
|
||||||
|
->withClaim('user_id', $request->user()->id)
|
||||||
|
->withClaim('server_uuid', $server->uuid)
|
||||||
|
->getToken($signer, new Key($server->node->daemonSecret));
|
||||||
|
|
||||||
$socket = str_replace(['https://', 'http://'], ['wss://', 'ws://'], $server->node->getConnectionAddress());
|
$socket = str_replace(['https://', 'http://'], ['wss://', 'ws://'], $server->node->getConnectionAddress());
|
||||||
|
|
||||||
return JsonResponse::create([
|
return JsonResponse::create([
|
||||||
'data' => [
|
'data' => [
|
||||||
'socket' => $socket . sprintf('/api/servers/%s/ws/%s', $server->uuid, $token),
|
'token' => $token->__toString(),
|
||||||
|
'socket' => $socket . sprintf('/api/servers/%s/ws', $server->uuid),
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,83 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Pterodactyl\Http\Controllers\Api\Remote;
|
|
||||||
|
|
||||||
use Illuminate\Http\Response;
|
|
||||||
use Illuminate\Contracts\Cache\Repository;
|
|
||||||
use Pterodactyl\Http\Controllers\Controller;
|
|
||||||
use Pterodactyl\Repositories\Eloquent\UserRepository;
|
|
||||||
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
||||||
use Pterodactyl\Http\Requests\Api\Remote\AuthenticateWebsocketDetailsRequest;
|
|
||||||
|
|
||||||
class ValidateWebsocketController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var \Illuminate\Contracts\Cache\Repository
|
|
||||||
*/
|
|
||||||
private $cache;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
|
|
||||||
*/
|
|
||||||
private $serverRepository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Repositories\Eloquent\UserRepository
|
|
||||||
*/
|
|
||||||
private $userRepository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ValidateWebsocketController constructor.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Contracts\Cache\Repository $cache
|
|
||||||
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $serverRepository
|
|
||||||
* @param \Pterodactyl\Repositories\Eloquent\UserRepository $userRepository
|
|
||||||
*/
|
|
||||||
public function __construct(Repository $cache, ServerRepository $serverRepository, UserRepository $userRepository)
|
|
||||||
{
|
|
||||||
$this->cache = $cache;
|
|
||||||
$this->serverRepository = $serverRepository;
|
|
||||||
$this->userRepository = $userRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Route allowing the Wings daemon to validate that a websocket route request is
|
|
||||||
* valid and that the given user has permission to access the resource.
|
|
||||||
*
|
|
||||||
* @param \Pterodactyl\Http\Requests\Api\Remote\AuthenticateWebsocketDetailsRequest $request
|
|
||||||
* @param string $token
|
|
||||||
* @return \Illuminate\Http\Response
|
|
||||||
*
|
|
||||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
|
||||||
*/
|
|
||||||
public function __invoke(AuthenticateWebsocketDetailsRequest $request, string $token)
|
|
||||||
{
|
|
||||||
$server = $this->serverRepository->getByUuid($request->input('server_uuid'));
|
|
||||||
if (! $data = $this->cache->pull('ws:' . $token)) {
|
|
||||||
throw new NotFoundHttpException;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var \Pterodactyl\Models\User $user */
|
|
||||||
$user = $this->userRepository->find($data['user_id']);
|
|
||||||
if (! $user->can('connect-to-ws', $server)) {
|
|
||||||
throw new HttpException(Response::HTTP_FORBIDDEN, 'You do not have permission to access this resource.');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var \Pterodactyl\Models\Node $node */
|
|
||||||
$node = $request->attributes->get('node');
|
|
||||||
|
|
||||||
if (
|
|
||||||
$data['server_id'] !== $server->id
|
|
||||||
|| $node->id !== $server->node_id
|
|
||||||
// @todo this doesn't work well in dev currently, need to look into this way more.
|
|
||||||
// @todo stems from some issue with the way requests are being proxied.
|
|
||||||
// || $data['request_ip'] !== $request->input('originating_request_ip')
|
|
||||||
) {
|
|
||||||
throw new HttpException(Response::HTTP_BAD_REQUEST, 'The token provided is not valid for the requested resource.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response::create('', Response::HTTP_NO_CONTENT);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -26,6 +26,7 @@
|
||||||
"laravel/framework": "^6.0.0",
|
"laravel/framework": "^6.0.0",
|
||||||
"laravel/helpers": "^1.1",
|
"laravel/helpers": "^1.1",
|
||||||
"laravel/tinker": "^1.0",
|
"laravel/tinker": "^1.0",
|
||||||
|
"lcobucci/jwt": "^3.3",
|
||||||
"matriphe/iso-639": "^1.2",
|
"matriphe/iso-639": "^1.2",
|
||||||
"pragmarx/google2fa": "^5.0",
|
"pragmarx/google2fa": "^5.0",
|
||||||
"predis/predis": "^1.1",
|
"predis/predis": "^1.1",
|
||||||
|
|
57
composer.lock
generated
57
composer.lock
generated
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "48b992ce56210c000f2d9a55a1c597e6",
|
"content-hash": "54a69da316f2921ebcae63ec6b054468",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "appstract/laravel-blade-directives",
|
"name": "appstract/laravel-blade-directives",
|
||||||
|
@ -1466,6 +1466,61 @@
|
||||||
],
|
],
|
||||||
"time": "2019-08-07T15:10:45+00:00"
|
"time": "2019-08-07T15:10:45+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "lcobucci/jwt",
|
||||||
|
"version": "3.3.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/lcobucci/jwt.git",
|
||||||
|
"reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/lcobucci/jwt/zipball/a11ec5f4b4d75d1fcd04e133dede4c317aac9e18",
|
||||||
|
"reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"ext-openssl": "*",
|
||||||
|
"php": "^5.6 || ^7.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"mikey179/vfsstream": "~1.5",
|
||||||
|
"phpmd/phpmd": "~2.2",
|
||||||
|
"phpunit/php-invoker": "~1.1",
|
||||||
|
"phpunit/phpunit": "^5.7 || ^7.3",
|
||||||
|
"squizlabs/php_codesniffer": "~2.3"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "3.1-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Lcobucci\\JWT\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"BSD-3-Clause"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Luís Otávio Cobucci Oblonczyk",
|
||||||
|
"email": "lcobucci@gmail.com",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A simple library to work with JSON Web Token and JSON Web Signature",
|
||||||
|
"keywords": [
|
||||||
|
"JWS",
|
||||||
|
"jwt"
|
||||||
|
],
|
||||||
|
"time": "2019-05-24T18:30:49+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "league/flysystem",
|
"name": "league/flysystem",
|
||||||
"version": "1.0.55",
|
"version": "1.0.55",
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
import http from '@/api/http';
|
import http from '@/api/http';
|
||||||
|
|
||||||
export default (server: string): Promise<string> => {
|
interface Response {
|
||||||
|
token: string;
|
||||||
|
socket: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (server: string): Promise<Response> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.get(`/api/client/servers/${server}/websocket`)
|
http.get(`/api/client/servers/${server}/websocket`)
|
||||||
.then(response => resolve(response.data.data.socket))
|
.then(({ data }) => resolve({
|
||||||
|
token: data.data.token,
|
||||||
|
socket: data.data.socket,
|
||||||
|
}))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Websocket } from '@/plugins/Websocket';
|
import { Websocket } from '@/plugins/Websocket';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
|
import getWebsocketToken from '@/api/server/getWebsocketToken';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const server = ServerContext.useStoreState(state => state.server.data);
|
const server = ServerContext.useStoreState(state => state.server.data);
|
||||||
|
@ -15,15 +16,18 @@ export default () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const socket = new Websocket(server.uuid);
|
const socket = new Websocket();
|
||||||
|
|
||||||
socket.on('SOCKET_OPEN', () => setConnectionState(true));
|
socket.on('SOCKET_OPEN', () => setConnectionState(true));
|
||||||
socket.on('SOCKET_CLOSE', () => setConnectionState(false));
|
socket.on('SOCKET_CLOSE', () => setConnectionState(false));
|
||||||
socket.on('SOCKET_ERROR', () => setConnectionState(false));
|
socket.on('SOCKET_ERROR', () => setConnectionState(false));
|
||||||
socket.on('status', (status) => setServerStatus(status));
|
socket.on('status', (status) => setServerStatus(status));
|
||||||
|
|
||||||
socket.connect()
|
getWebsocketToken(server.uuid)
|
||||||
.then(() => setInstance(socket))
|
.then(data => {
|
||||||
|
socket.setToken(data.token).connect(data.socket);
|
||||||
|
setInstance(socket);
|
||||||
|
})
|
||||||
.catch(error => console.error(error));
|
.catch(error => console.error(error));
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -36,8 +40,8 @@ export default () => {
|
||||||
// exist outside of dev? Will need to see how things go.
|
// exist outside of dev? Will need to see how things go.
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!connected && instance) {
|
if (!connected && instance && instance.getToken() && instance.getSocketUrl()) {
|
||||||
instance.connect();
|
instance.connect(instance.getSocketUrl()!);
|
||||||
}
|
}
|
||||||
}, [ connected ]);
|
}, [ connected ]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import Sockette from 'sockette';
|
import Sockette from 'sockette';
|
||||||
import getWebsocketToken from '@/api/server/getWebsocketToken';
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
export const SOCKET_EVENTS = [
|
export const SOCKET_EVENTS = [
|
||||||
|
@ -10,37 +9,54 @@ export const SOCKET_EVENTS = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export class Websocket extends EventEmitter {
|
export class Websocket extends EventEmitter {
|
||||||
private socket: Sockette | null;
|
// The socket instance being tracked.
|
||||||
private readonly uuid: string;
|
private socket: Sockette | null = null;
|
||||||
|
|
||||||
constructor (uuid: string) {
|
// The URL being connected to for the socket.
|
||||||
super();
|
private url: string | null = null;
|
||||||
|
|
||||||
this.socket = null;
|
// The authentication token passed along with every request to the Daemon.
|
||||||
this.uuid = uuid;
|
// By default this token expires every 15 minutes and must therefore be
|
||||||
|
// refreshed at a pretty continuous interval. The socket server will respond
|
||||||
|
// with "token expiring" and "token expired" events when approaching 3 minutes
|
||||||
|
// and 0 minutes to expiry.
|
||||||
|
private token: string = '';
|
||||||
|
|
||||||
|
// Connects to the websocket instance and sets the token for the initial request.
|
||||||
|
connect (url: string) {
|
||||||
|
this.url = url;
|
||||||
|
this.socket = new Sockette(url, {
|
||||||
|
onmessage: e => {
|
||||||
|
try {
|
||||||
|
let { event, args } = JSON.parse(e.data);
|
||||||
|
this.emit(event, ...args);
|
||||||
|
} catch (ex) {
|
||||||
|
console.warn('Failed to parse incoming websocket message.', ex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onopen: () => this.emit('SOCKET_OPEN'),
|
||||||
|
onreconnect: () => this.emit('SOCKET_RECONNECT'),
|
||||||
|
onclose: () => this.emit('SOCKET_CLOSE'),
|
||||||
|
onerror: () => this.emit('SOCKET_ERROR'),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect (): Promise<void> {
|
// Returns the URL connected to for the socket.
|
||||||
getWebsocketToken(this.uuid)
|
getSocketUrl (): string | null {
|
||||||
.then(url => {
|
return this.url;
|
||||||
this.socket = new Sockette(url, {
|
}
|
||||||
onmessage: e => {
|
|
||||||
try {
|
|
||||||
let { event, args } = JSON.parse(e.data);
|
|
||||||
this.emit(event, ...args);
|
|
||||||
} catch (ex) {
|
|
||||||
console.warn('Failed to parse incoming websocket message.', ex);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onopen: () => this.emit('SOCKET_OPEN'),
|
|
||||||
onreconnect: () => this.emit('SOCKET_RECONNECT'),
|
|
||||||
onclose: () => this.emit('SOCKET_CLOSE'),
|
|
||||||
onerror: () => this.emit('SOCKET_ERROR'),
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.resolve();
|
// Sets the authentication token to use when sending commands back and forth
|
||||||
})
|
// between the websocket instance.
|
||||||
.catch(error => Promise.reject(error));
|
setToken (token: string): this {
|
||||||
|
this.token = token;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the token being used at the current moment.
|
||||||
|
getToken (): string {
|
||||||
|
return this.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
close (code?: number, reason?: string) {
|
close (code?: number, reason?: string) {
|
||||||
|
@ -57,7 +73,9 @@ export class Websocket extends EventEmitter {
|
||||||
|
|
||||||
send (event: string, payload?: string | string[]) {
|
send (event: string, payload?: string | string[]) {
|
||||||
this.socket && this.socket.send(JSON.stringify({
|
this.socket && this.socket.send(JSON.stringify({
|
||||||
event, args: Array.isArray(payload) ? payload : [ payload ],
|
event,
|
||||||
|
args: Array.isArray(payload) ? payload : [ payload ],
|
||||||
|
token: this.token || '',
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/authenticate/{token}', 'ValidateKeyController@index');
|
Route::get('/authenticate/{token}', 'ValidateKeyController@index');
|
||||||
Route::post('/websocket/{token}', 'ValidateWebsocketController');
|
|
||||||
Route::post('/download-file', 'FileDownloadController@index');
|
Route::post('/download-file', 'FileDownloadController@index');
|
||||||
|
|
||||||
Route::group(['prefix' => '/scripts'], function () {
|
Route::group(['prefix' => '/scripts'], function () {
|
||||||
|
|
Loading…
Reference in a new issue