I need to add log after call http request in big project like this?
$response = Http::get('http://example.com');
Log::info(`add request and header and response`);
i want to define global log for all http requests.
i need to define macro like this :
\Illuminate\Support\Facades\Http::macro('log',function(){
Log::info(`add request and header and response`);
});
and call http request like this:
$response = Http::get('http://example.com')->log();
Http is built on Guzzle, which accepts cURL options. One of those is CURLOPT_VERBOSE, rewritten as debug, which will send request data to either the screen or a log file. It accepts a file resource as an option:
$response = Http::withOptions(['debug'=>true])->get('http://example.com');
Or
$fp = fopen(storage_path('http_log.txt'), 'w+');
$response = Http::withOptions(['debug'=>$fp])->get('http://example.com');
If you need more data than that, you can extend the Http class and add your own logging methods to it.
See https://laravel.com/docs/8.x/http-client#guzzle-options and https://docs.guzzlephp.org/en/stable/request-options.html#debug for information on the debug option.
You can use a Terminable Middleware to log the HTTP response after it has already been sent to the browser.
To get the total time you can compare the result of microtime(true) with the laravel constant LARAVEL_START. That constant is defined at bootstrap/autoload.php, the entry point of the framework
For instance, here is a middleware that will log in both HTTP headers and system log the response time. Since you have access to the current request in the $request variable you could leverage that to also log any parameters you want
<?php // File: app/Http/Middleware/MeasureResponseTime.php
namespace App\Http\Middleware;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class MeasureResponseTime
{
/**
* Handle an incoming HTTP request.
*
* #param \Symfony\Component\HttpFoundation\Request $request
* #param \Closure $next
* #return \Symfony\Component\HttpFoundation\Response
*/
public function handle($request, \Closure $next)
{
$response = $next($request);
// Add response time as an HTTP header. For better accuracy ensure this middleware
// is added at the end of the list of global middlewares in the Kernel.php file
if (defined('LARAVEL_START') and $response instanceof Response) {
$response->headers->add(['X-RESPONSE-TIME' => microtime(true) - LARAVEL_START]);
}
return $response;
}
/**
* Perform any final actions for the request lifecycle.
*
* #param \Symfony\Component\HttpFoundation\Request $request
* #param \Symfony\Component\HttpFoundation\Response $response
* #return void
*/
public function terminate($request, $response)
{
// At this point the response has already been sent to the browser so any
// modification to the response (such adding HTTP headers) will have no effect
if (defined('LARAVEL_START') and $request instanceof Request) {
app('log')->debug('Response time', [
'method' => $request->getMethod(),
'uri' => $request->getRequestUri(),
'seconds' => microtime(true) - LARAVEL_START,
]);
}
}
}
Related
I have a problem with my application Lumen-VueJs
I make a request and the request status is 200 and I receive what I want but on the 'network' the request is blocked. ( screen )
I have on my app a CorsMiddleware that is like that and that is also added in the bootstrap/app.php
<?php
/**
* Location: /app/Http/Middleware
*/
namespace App\Http\Middleware;
use Closure;
class CorsMiddleware
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
$headers = [
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Methods' => 'POST,GET,PATCH,PUT,DELETE,OPTIONS',
'Access-Control-Max-Age' => '86400',
'Access-Control-Allow-Headers' => 'Content-Type,API-KEY'
];
if ($request->isMethod('OPTIONS')) {
return response()->json('', 200, $headers);
}
$response = $next($request);
foreach ($headers as $key => $value) {
$response->header($key, $value);
}
return $response;
}
}
These are the headers of my request :
I don't understand why I have that error that must be authorized by my Middleware
Thanks in advance for your help !
Summarizing our discussion in the chat, the issue your browser is complaining about is correct. The Access-Control-Allow-Origin header is not sent.
This is because your middleware was not called as you have added your middleware to $app->routeMiddleware with key cors. I assume there is no route that is called cors. From Lumen's GitHub:
These can be global middleware that run before and after each request [$middleware] into a route or middleware that'll be assigned to some specific routes [$routeMiddleware].
As your CORS middleware should be called for all requests, you need to add it to $app->middleware.
I'm working on a Slim 3 based application with a Twig frontend and I'm also making a REST API.
I've implemented slimphp\Slim-Csrf for the entire app but I now want to exclude this CSRF check from every "API" routes.
I'm trying to implement the "Option 2" of this post :
Slim3 exclude route from CSRF Middleware
Here is the code :
File App\Middleware\CsrfMiddleware.php :
namespace App\Middleware;
class CsrfMiddleware extends \Slim\Csrf\Guard {
public function processRequest($request, $response, $next) {
// Check if this route is in the "Whitelist"
$route = $request->getAttribute('route');
if ($route->getName() == 'token') {
var_dump('! problem HERE, this middleware is executed after the CsrfMiddleware !');
// supposed to SKIP \Slim\Csrf\Guard
return $next($request, $response);
} else {
// supposed to execute \Slim\Csrf\Guard
return $this($request, $response, $next);
}
}
}
File app\app.php :
$app = new \Slim\App([
'settings' => [
'determineRouteBeforeAppMiddleware' => true
]
]);
require('container.php');
require('routes.php');
$app->add($container->csrf);
$app->add('csrf:processRequest');
File app\container.php :
$container['csrf'] = function ($container) {
return new App\Middleware\CsrfMiddleware;
};
File app\routes.php :
<?php
$app->get('/', \App\PagesControllers\LieuController::class.':home')->setName('home');
$app->post('/api/token', \App\ApiControllers\AuthController::class.'postToken')->setName('token');
When I do a POST request on http://localhost/slim3/public/api/token I've got :
Failed CSRF check!string(70) "! problem HERE, this middleware is executed after the CsrfMiddleware !"
Like if my CsrfMiddleware was executed after \Slim\Csrf\Guard...
Anyone has an idea ?
Thank you.
In Slim 3 the middleware is LIFO (last in first out).
Add the middleware in the opposite direction:
Before
$app->add($container->csrf);
$app->add('csrf:processRequest');
After
$app->add('csrf:processRequest');
$app->add($container->csrf);
Notice: The public directory should not be part of the url
Not correct: http://localhost/slim3/public/api/token
Correct: http://localhost/slim3/api/token
To skip the processing within the middleware, just return the $response object.
// supposed to SKIP \Slim\Csrf\Guard
return $response;
Here is how I achieved this with Slim 3.
1) Create a class that extends \Slim\Csrf\Guard as follows.
The CsrfGuardOverride class is key to enabling or disabling CSRF checking for a path. If the current path is whitelist'ed, then the __invoke() method just skips the core CSRF checking, and proceeds by executing the next middleware layer.
If the current path is not in the whitelist (i.e., CSRF should be checked), then the __invoke method defers to its parent \Slim\Csrf\Guard::__invoke() to handle CSRF in the normal manner.
<?php
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use \Slim\Csrf\Guard;
class CsrfGuardOverride extends Guard {
/**
* Invoke middleware
*
* #param ServerRequestInterface $request PSR7 request object
* #param ResponseInterface $response PSR7 response object
* #param callable $next Next middleware callable
*
* #return ResponseInterface PSR7 response object
*/
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next)
{
// Set the name of the route we want whitelisted with a name
// prefix of 'whitelist'. check for that here, and add
// any path to the white list
$route = $request->getAttribute('route');
$routeName = $route->getName();
$whitelisted = strpos($routeName, 'whitelist');
// if url is whitelisted from being CSRF checked, then bypass checking by skipping directly to next middleware
if ($whitelisted !== FALSE) {
return $next($request, $response);
}
return parent::__invoke($request, $response, $next);
}
}
2) Register the CsrfGuardOverride class. Be sure to set settings.determineRouteBeforeAppMiddleware => true as this forces Slim to evaluate routes prior to executing any middleware.
// Method on App Class
protected function configureContainer(ContainerBuilder $builder)
{
parent::configureContainer($builder);
$definitions = [
'settings.displayErrorDetails' => true,
'settings.determineRouteBeforeAppMiddleware' => true,
// Cross-Site Request Forgery protection
\App\Middleware\CsrfGuardOverride::class => function (ContainerInterface $container) {
$guard = new \App\Middleware\CsrfGuardOverride;
$guard->setPersistentTokenMode(true); // allow same CSRF token for multiple ajax calls per session
return $guard;
},
'csrf' => DI\get(\App\Middleware\CsrfGuardOverride::class),
// add others here...
];
$builder->addDefinitions($definitions);
}
3) Add the path that you want to by bypass CSRF checking and give it a name with the prefix 'whitelist':
$app->post('/events/purchase', ['\App\Controllers\PurchaseController', 'purchaseCallback'])->setName('whitelist.events.purchase');
I'm writing a tiny sms gateway to be consumed by a couple of projects,
I implemented laravel passport authentication (client credentials grant token)
Then I've added CheckClientCredentials to api middleware group:
protected $middlewareGroups = [
'web' => [
...
],
'api' => [
'throttle:60,1',
'bindings',
\Laravel\Passport\Http\Middleware\CheckClientCredentials::class
],
];
The logic is working fine, now in my controller I need to get client associated with a valid token.
routes.php
Route::post('/sms', function(Request $request) {
// save the sms along with the client id and send it
$client_id = ''; // get the client id somehow
sendSms($request->text, $request->to, $client_id);
});
For obvious security reasons I can never send the client id with the consumer request e.g. $client_id = $request->client_id;.
I use this, to access the authenticated client app...
$bearerToken = $request->bearerToken();
$tokenId = (new \Lcobucci\JWT\Parser())->parse($bearerToken)->getHeader('jti');
$client = \Laravel\Passport\Token::find($tokenId)->client;
$client_id = $client->id;
$client_secret = $client->secret;
Source
However the answer is quite late, i got some errors extracting the JTI header
in Laravel 6.x because the JTI is no longer in the header, but only in the payload/claim. (Using client grants)
local.ERROR: Requested header is not configured {"exception":"[object] (OutOfBoundsException(code: 0): Requested header is not configured at /..somewhere/vendor/lcobucci/jwt/src/Token.php:112)
Also, adding it in a middleware was not an option for me. As i needed it on several places in my app.
So i extended the original Laravel Passport Client (oauth_clients) model.
And check the header as well as the payload. Allowing to pass a request, or use
the request facade, if no request was passed.
<?php
namespace App\Models;
use Illuminate\Support\Facades\Request as RequestFacade;
use Illuminate\Http\Request;
use Laravel\Passport\Client;
use Laravel\Passport\Token;
use Lcobucci\JWT\Parser;
class OAuthClient extends Client
{
public static function findByRequest(?Request $request = null) : ?OAuthClient
{
$bearerToken = $request !== null ? $request->bearerToken() : RequestFacade::bearerToken();
$parsedJwt = (new Parser())->parse($bearerToken);
if ($parsedJwt->hasHeader('jti')) {
$tokenId = $parsedJwt->getHeader('jti');
} elseif ($parsedJwt->hasClaim('jti')) {
$tokenId = $parsedJwt->getClaim('jti');
} else {
Log::error('Invalid JWT token, Unable to find JTI header');
return null;
}
$clientId = Token::find($tokenId)->client->id;
return (new static)->findOrFail($clientId);
}
}
Now you can use it anywhere inside your laravel app like this:
If you have $request object available, (for example from a controller)
$client = OAuthClient::findByRequest($request);
Or even if the request is not available somehow, you can use it without, like this:
$client = OAuthClient::findByRequest();
Hopefully this useful for anyone, facing this issue today.
There is a tricky method.
You can modify the method of handle in the middleware CheckClientCredentials, just add this line.
$request["oauth_client_id"] = $psr->getAttribute('oauth_client_id');
Then you can get client_id in controller's function:
public function info(\Illuminate\Http\Request $request)
{
var_dump($request->oauth_client_id);
}
The OAuth token and client information are stored as a protected variable in the Laravel\Passport\HasApiTokens trait (which you add to your User model).
So simply add a getter method to your User model to expose the OAuth information:
public function get_oauth_client(){
return $this->accessToken->client;
}
This will return an Eloquent model for the oauth_clients table
In the latest implementation you can use:
use Laravel\Passport\Token;
use Lcobucci\JWT\Configuration;
$bearerToken = request()->bearerToken();
$tokenId = Configuration::forUnsecuredSigner()->parser()->parse($bearerToken)->claims()->get('jti');
$client = Token::find($tokenId)->client;
as suggested here: https://github.com/laravel/passport/issues/124#issuecomment-784731969
So, no answers ...
I was able to resolve the issue by consuming my own API, finally I came up with simpler authentication flow, the client need to send their id & secret with each request, then I consumed my own /oauth/token route with the sent credentials, inspired by Esben Petersen blog post.
Once the access token is generated, I append it to the headers of Symfony\Request instance which is under processing.
My final output like this:
<?php
namespace App\Http\Middleware;
use Request;
use Closure;
class AddAccessTokenHeader
{
/**
* Octipus\ApiConsumer
* #var ApiConsumer
*/
private $apiConsumer;
function __construct() {
$this->apiConsumer = app()->make('apiconsumer');
}
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
$response = $this->apiConsumer->post('/oauth/token', $request->input(), [
'content-type' => 'application/json'
]);
if (!$response->isSuccessful()) {
return response($response->getContent(), 401)
->header('content-type', 'application/json');
}
$response = json_decode($response->getContent(), true);
$request->headers->add([
'Authorization' => 'Bearer ' . $response['access_token'],
'X-Requested-With' => 'XMLHttpRequest'
]);
return $next($request);
}
}
I used the above middleware in conjunction with Passport's CheckClientCredentials.
protected $middlewareGroups = [
'web' => [
...
],
'api' => [
'throttle:60,1',
'bindings',
\App\Http\Middleware\AddAccessTokenHeader::class,
\Laravel\Passport\Http\Middleware\CheckClientCredentials::class
],
];
This way, I was able to insure that $request->input('client_id') is reliable and can't be faked.
I dug into CheckClientCredentials class and extracted what I needed to get the client_id from the token. aud claim is where the client_id is stored.
<?php
Route::middleware('client')->group(function() {
Route::get('/client-id', function (Request $request) {
$jwt = trim(preg_replace('/^(?:\s+)?Bearer\s/', '', $request->header('authorization')));
$token = (new \Lcobucci\JWT\Parser())->parse($jwt);
return ['client_id' => $token->getClaim('aud')];
});
});
Few places to refactor this to in order to easily access but that will be up to your application
As I can see the above answer are old and most importantly it dose not work with laravel 8 and php 8, so I have found a way to get the client id of the access token ( current request )
the answer is basically making a middleware, and add it to all routes you want to get the client id.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Nyholm\Psr7\Factory\Psr17Factory;
use Laravel\Passport\TokenRepository;
use League\OAuth2\Server\ResourceServer;
use Illuminate\Auth\AuthenticationException;
use League\OAuth2\Server\Exception\OAuthServerException;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
class SetPassportClient
{
/**
* The Resource Server instance.
*
* #var \League\OAuth2\Server\ResourceServer
*/
protected $server;
/**
* Token Repository.
*
* #var \Laravel\Passport\TokenRepository
*/
protected $repository;
/**
* Create a new middleware instance.
*
* #param \League\OAuth2\Server\ResourceServer $server
* #param \Laravel\Passport\TokenRepository $repository
* #return void
*/
public function __construct(ResourceServer $server, TokenRepository $repository)
{
$this->server = $server;
$this->repository = $repository;
}
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle(Request $request, Closure $next)
{
$psr = (new PsrHttpFactory(
new Psr17Factory,
new Psr17Factory,
new Psr17Factory,
new Psr17Factory
))->createRequest($request);
try {
$psr = $this->server->validateAuthenticatedRequest($psr);
} catch (OAuthServerException $e) {
throw new AuthenticationException;
}
$token = $this->repository->find($psr->getAttribute('oauth_access_token_id'));
if (!$token)
abort(401);
$request->merge(['passportClientId' => $token->client_id]);
return $next($request);
}
}
Add the middleware to app\Http\Kernel.php
protected $routeMiddleware = [
.
.
'passport.client.set' => \App\Http\Middleware\SetPassportClient::class
];
Finaly in the routes add the middleware
Route::middleware(['client', 'passport.client.set'])->get('/test-client-id', function (Request $request){
dd($request->passportClientId); // this the client id
});
Sorry for the long answer, but I want it to be very clear to any all.
All of the code was inspired by laravel CheckCredentials.php
public function handle($request, Closure $next, $scope)
{
if (!empty($scope)) {
$psr = (new DiactorosFactory)->createRequest($request);
$psr = $this->server->validateAuthenticatedRequest($psr);
$clientId = $psr->getAttribute('oauth_client_id');
$request['oauth_client_id'] = intval($clientId);
}
return $next($request);
}
put above to your middleware file, then you can access client_id by request()->oauth_client_id
In a method you can easily get by:
$token = $request->user()->token();
$clientId = $token['client_id'];
I'm using Behat in a Symfony2 app.
I have made a reusable action to add a HTTP header on some scenarios
/**
* #Given I am authenticated as admin
*/
public function iAmAuthenticatedAsAdmin()
{
$value = 'Bearer xxxxxxxxxxx';
$response = new Response();
$response->headers->set('Authorization', $value);
$response->send();
return $response;
}
This action is call when I add the I am authenticated as admin step in my scenario but it doesn't add my header. Like this
Scenario: I find all my DNS zones
Given I am authenticated as admin
And I send a GET request to "/api/dns"
Then the response code should be 200
How can I add a HTTP header before my request step in my scenario, using a reusable action ?
Is it possible ?
Thank.
I have find the way to do this.
If you are using WebApiExtension
Just import the WebApiContext in your context class like this
/**
* #param BeforeScenarioScope $scope
*
* #BeforeScenario
*/
public function gatherContexts(BeforeScenarioScope $scope)
{
$environment = $scope->getEnvironment();
$this->webApiContext = $environment->getContext('Behat\WebApiExtension\Context\WebApiContext');
}
And you just have now to use the iSetHeaderWithValue :
/**
* #Given I am authenticated as admin
*/
public function iAmAuthenticatedAsAdmin()
{
$name = 'Authorization';
$value = 'Bearer xxxxxxxx';
$this->webApiContext->iSetHeaderWithValue($name, $value);
}
Why don't you simply store it in session (hint: session should be destroyed at every scenario; take a look to BeforeScenario) and use whenever you need it?
Because I guess it's added but, on next request, it's gone as a brand new pair of request/response are generated (if I understand your needs correctly)
I currently ran into the problem of handling the authentification of a user on the server, using Laravel and RachetPHP.
What I tried so far:
I changed the driver type of the session to database, giving me an id and payload column. Using \Session::getId() returns a 40 character string.
The cookie information, sent by the WebSocket-Connection does contain a XSRF-TOKEN and a laravel_session, both containing > 200 characters string. The database ID of the users session differs from the id, returned by \Session::getId().
I am already sending the current CSRF-token via the websocket message, but I have no clue how to verify it (the built-in verifier uses requests - which I don't have in the websocket server scope).
Generic Use case:
A User posts a comment in thread. The payload of the sent object would then be:
Something to verify the user (an ID or a token).
The comment itself
If you were to send the user ID, anyone could temper the packet and send the message under another ones user.
My use case:
A user can have n-characters. A character has an avatar, an id, a name, etc.
The user is only used to:
authenticate at the server.
access his characters, and thus perform basic CRUD operations on his characters.
I also have a table locations - a "virtual place", a character can be in... so I got a one-to-one relationship between character and location. The user (character) can then send messages in a location via websocket. The message is inserted at the database on the server. At this point, I need to know:
If the user is authenticated (csrf-token ?)
If the user is the owner of the character (it's very simple to spoof the request with another user's character id)
If you need more information, please let me know.
So this is how I solved this a while ago. In my example, I'm working with Socket.IO, but I'm pretty sure you can easily rewrite the Socket.IO part to get it to work with RachetPHP as well.
Socket Server
The socket server depends on the files cookie.js and array.js, and the node modules express, http, socket.io, request and dotenv. I'm not the original author of cookie.js, but there is no author mentioned in the comments, so I'm not able to give any credits for this, sorry.
This is the server.js file which starts the server. It is a simple socket server that tracks who is currently online. The interesting part however is when the server makes a POST request to socket/auth on the Laravel application:
var express = require('express');
var app = express();
var server = require('http').Server(app)
var io = require('socket.io')(server);
var request = require('request');
var co = require('./cookie.js');
var array = require('./array.js');
// This loads the Laravel .env file
require('dotenv').config({path: '../.env'});
server.listen(process.env.SOCKET_SERVER_PORT);
var activeSockets = {};
var disconnectTimeouts = {};
// When a client connects
io.on('connection', function(socket)
{
console.log('Client connected...');
// Read the laravel_session cookie.
var cookieManager = new co.cookie(socket.handshake.headers.cookie);
var sess = cookieManager.get("laravel_session"); // Rename "laravel_session" to whatever you called it
// This is where the socket asks the Laravel app to authenticate the user
request.post('http://' + process.env.SOCKET_SERVER_HOST + '/socket/auth?s=' + sess, function(error, response, body)
{
try {
// Parse the response from the server
body = JSON.parse(body);
}
catch(e)
{
console.log('Error while parsing JSON', e);
error = true;
}
if ( ! error && response.statusCode == 200 && body.authenticated)
{
// Assign users ID to the socket
socket.userId = body.user.id;
if ( ! array.contains(activeSockets, socket.userId))
{
// The client is now 'active'
activeSockets.push(socket.userId);
var message = body.user.firstname + ' is now online!';
console.log(message);
// Tell everyone that the user has joined
socket.broadcast.emit('userJoined', socket.userId);
}
else if (array.hasKey(disconnectTimeouts, 'user_' + socket.userId))
{
clearTimeout(disconnectTimeouts['user_' + socket.userId]);
delete disconnectTimeouts['user_id' + socket.userId];
}
socket.on('disconnect', function()
{
// The client is 'inactive' if he doesn't reastablish the connection within 10 seconds
// For a 'who is online' list, this timeout ensures that the client does not disappear and reappear on each page reload
disconnectTimeouts['user_' + socket.userId] = setTimeout(function()
{
delete disconnectTimeouts['user_' + socket.userId];
array.remove(activeSockets, socket.userId);
var message = body.user.firstname + ' is now offline.';
console.log(message);
socket.broadcast.emit('userLeft', socket.userId);
}, 10000);
});
}
});
});
I added some comments to the code, so it should be pretty self-explanatory. Please note that I added SOCKET_SERVER_HOST and SOCKET_SERVER_PORT to my Laravel .env-file in order to be able to change the host and port without editing the code and run the server on different environments.
SOCKET_SERVER_HOST = localhost
SOCKET_SERVER_PORT = 1337
Authenticating a user by a session cookie with Laravel
This is the SocketController which parses the cookie and responds whether the user could be authenticated or not (JSON response). Its the same mechanism that you described in your answer. It's not the best design to handle the cookie parsing in the controller, but it should be OK in this case, because the controller only handles that one thing and its functionality isn't used at another point in the application.
/app/Http/Controllers/SocketController.php
<?php namespace App\Http\Controllers;
use App\Http\Requests;
use App\Users\UserRepositoryInterface;
use Illuminate\Auth\Guard;
use Illuminate\Database\DatabaseManager;
use Illuminate\Encryption\Encrypter;
use Illuminate\Http\Request;
use Illuminate\Routing\ResponseFactory;
/**
* Class SocketController
* #package App\Http\Controllers
*/
class SocketController extends Controller {
/**
* #var Encrypter
*/
private $encrypter;
/**
* #var DatabaseManager
*/
private $database;
/**
* #var UserRepositoryInterface
*/
private $users;
/**
* Initialize a new SocketController instance.
*
* #param Encrypter $encrypter
* #param DatabaseManager $database
* #param UserRepositoryInterface $users
*/
public function __construct(Encrypter $encrypter, DatabaseManager $database, UserRepositoryInterface $users)
{
parent::__construct();
$this->middleware('internal');
$this->encrypter = $encrypter;
$this->database = $database;
$this->users = $users;
}
/**
* Authorize a user from node.js socket server.
*
* #param Request $request
* #param ResponseFactory $response
* #param Guard $auth
* #return \Symfony\Component\HttpFoundation\Response
*/
public function authenticate(Request $request, ResponseFactory $response, Guard $auth)
{
try
{
$payload = $this->getPayload($request->get('s'));
} catch (\Exception $e)
{
return $response->json([
'authenticated' => false,
'message' => $e->getMessage()
]);
}
$user = $this->users->find($payload->{$auth->getName()});
return $response->json([
'authenticated' => true,
'user' => $user->toArray()
]);
}
/**
* Get session payload from encrypted laravel session.
*
* #param $session
* #return object
* #throws \Exception
*/
private function getPayload($session)
{
$sessionId = $this->encrypter->decrypt($session);
$sessionEntry = $this->getSession($sessionId);
$payload = base64_decode($sessionEntry->payload);
return (object) unserialize($payload);
}
/**
* Fetches base64 encoded session string from the database.
*
* #param $sessionId
* #return mixed
* #throws \Exception
*/
private function getSession($sessionId)
{
$sessionEntry = $this->database->connection()
->table('sessions')->select('*')->whereId($sessionId)->first();
if (is_null($sessionEntry))
{
throw new \Exception('The session could not be found. [Session ID: ' . $sessionId . ']');
}
return $sessionEntry;
}
}
In the constructor you can see that I refer to the internal middleware. I added this middleware to only allow the socket server to make requests to socket/auth.
This is what the middleware looks like:
/app/Http/Middleware/InternalMiddleware.php
<?php namespace App\Http\Middleware;
use Closure;
use Illuminate\Routing\ResponseFactory;
class InternalMiddleware {
/**
* #var ResponseFactory
*/
private $response;
/**
* #param ResponseFactory $response
*/
public function __construct(ResponseFactory $response)
{
$this->response = $response;
}
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if (preg_match(env('INTERNAL_MIDDLEWARE_IP'), $request->ip()))
{
return $next($request);
}
return $this->response->make('Unauthorized', 401);
}
}
To get this middleware to work, register it in the Kernel and add the INTERNAL_MIDDLEWARE_IP property - that is just a regular expression defining which IP addresses are allowed - to your .env-file:
Local testing (any IP):
INTERNAL_MIDDLEWARE_IP = /^.*$/
Production env:
INTERNAL_MIDDLEWARE_IP = /^192\.168\.0\.1$/
I'm sorry I could not help you out with RachetPHP, but I think you get a good idea how this can be solved.
I think I found a solution. Although not very clean, it does what it's supposed to do (I guess...)
The WebSocket-Server gets started by an Artisan Command (by mmochetti#github). I inject these classes into the Command:
Illuminate\Contracts\Encryption\Encrypter
App\Contracts\CsrfTokenVerifier - a custom CsrfTokenVerifier, that simply compares 2 strings (going to put more of the follow logic code in there)
I pass these instances from the command to the server. On the onMessage method, I parse the message sent, containing:
The CSRF-Token of the user
The character-id of the user
I then check if the token is valid, and if the user is the owner of the character.
public function onMessage(ConnectionInterface $from, NetworkMessage $message) {
if (!$this->verifyCsrfToken($from, $message)) {
throw new TokenMismatchException;
}
if (!$this->verifyUser($from, $message)) {
throw new \Exception('test');
}
...
}
private function verifyUser(ConnectionInterface $conn, NetworkMessage $message) {
$cookies = $conn->WebSocket->request->getCookies();
$laravel_session = rawurldecode($cookies['laravel_session']);
$id = $this->encrypter->decrypt($laravel_session);
$session = Session::find($id);
$payload = unserialize(base64_decode($session->payload));
$user_id = $payload['user_id'];
$user = User::find($user_id);
$characters = $this->characterService->allFrom($user);
$character_id = $message->getHeader()['character_id'];
return $characters->contains($character_id);
}
private function verifyCsrfToken($from, NetworkMessage $message) {
$header = $this->getHeaderToken($from);
return $this->verifier->tokensMatch($header, $message->getId());
}
The code could be cleaner for sure, but as a quick hack, it works. I think, instead of using a model for the Session, I should use the Laravel DatabaseSessionHandler.
For Laravel > 5 i use this code:
$cookies = $conn->WebSocket->request->getCookies();
$laravel_session = rawurldecode($cookies['laravel_session']);
$id = $this->encrypter->decrypt($laravel_session);
if(Config::get('session.driver', 'file') == 'file')
{
$session = File::get(storage_path('framework/sessions/' . $id));
}
$session = array_values(unserialize($session));
return $session[4]; // todo: Hack, please think another solution
To get cookies from client through websocket you must change domain in session config and change everywhere websocket host to your domain:
'domain' => 'your.domain.com',