Skip to content

Commit

Permalink
feat: implement session package
Browse files Browse the repository at this point in the history
  • Loading branch information
albertcht committed Jan 28, 2025
1 parent d237877 commit a047f4e
Show file tree
Hide file tree
Showing 36 changed files with 3,653 additions and 69 deletions.
6 changes: 5 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"SwooleTW\\Hyperf\\Prompts\\": "src/prompts/src/",
"SwooleTW\\Hyperf\\Queue\\": "src/queue/src/",
"SwooleTW\\Hyperf\\Router\\": "src/router/src/",
"SwooleTW\\Hyperf\\Session\\": "src/session/src/",
"SwooleTW\\Hyperf\\Support\\": "src/support/src/"
},
"files": [
Expand All @@ -60,6 +61,7 @@
"src/foundation/src/helpers.php",
"src/prompts/src/helpers.php",
"src/router/src/Functions.php",
"src/session/src/Functions.php",
"src/support/src/Functions.php",
"src/support/src/helpers.php"
]
Expand Down Expand Up @@ -132,6 +134,7 @@
"swooletw/hyperf-prompts": "self.version",
"swooletw/hyperf-queue": "self.version",
"swooletw/hyperf-router": "self.version",
"swooletw/hyperf-session": "self.version",
"swooletw/hyperf-support": "self.version"
},
"suggest": {
Expand Down Expand Up @@ -188,7 +191,8 @@
"SwooleTW\\Hyperf\\Mail\\ConfigProvider",
"SwooleTW\\Hyperf\\Notifications\\ConfigProvider",
"SwooleTW\\Hyperf\\Queue\\ConfigProvider",
"SwooleTW\\Hyperf\\Router\\ConfigProvider"
"SwooleTW\\Hyperf\\Router\\ConfigProvider",
"SwooleTW\\Hyperf\\Session\\ConfigProvider"
],
"providers": [
"SwooleTW\\Hyperf\\Notifications\\NotificationServiceProvider"
Expand Down
5 changes: 4 additions & 1 deletion src/devtool/src/Generator/stubs/sessions-table.stub
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ return new class extends Migration {
{
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->text('payload');
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
Expand Down
3 changes: 2 additions & 1 deletion src/foundation/src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,8 @@ protected function registerCoreContainerAliases(): void
\SwooleTW\Hyperf\Router\UrlGenerator::class => ['url'],
\Hyperf\ViewEngine\Contract\FactoryInterface::class => ['view'],
\Hyperf\ViewEngine\Compiler\CompilerInterface::class => ['blade.compiler'],
\Hyperf\Contract\SessionInterface::class => ['session'],
\Hyperf\Contract\SessionInterface::class => ['session.store'],
\SwooleTW\Hyperf\Session\Contracts\Factory::class => ['session'],
\SwooleTW\Hyperf\Foundation\Console\Contracts\Schedule::class => ['schedule'],
\SwooleTW\Hyperf\Mail\Contracts\Factory::class => [
'mail.manager',
Expand Down
66 changes: 66 additions & 0 deletions src/foundation/src/Http/Middleware/Concerns/ExcludesPaths.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace SwooleTW\Hyperf\Foundation\Http\Middleware\Concerns;

use Hyperf\Stringable\Str;
use Psr\Http\Message\ServerRequestInterface;

trait ExcludesPaths
{
/**
* Determine if the request has a URI that should be excluded.
*/
protected function inExceptArray(ServerRequestInterface $request): bool
{
$fullUrl = $this->getFullUrl($request);
$decodedUrl = $this->decodedPath($request);
foreach ($this->getExcludedPaths() as $except) {
if ($except !== '/') {
$except = trim($except, '/');
}

if (Str::is($except, $fullUrl) || Str::is($except, $decodedUrl)) {
return true;
}
}

return false;
}

/**
* Get the URIs that should be excluded.
*/
public function getExcludedPaths(): array
{
return $this->except ?? [];
}

/**
* Get the full URL for the request.
*/
protected function getFullUrl(ServerRequestInterface $request): string
{
$uri = $request->getUri();
$query = $uri->getQuery();

$baseUrl = $uri->getScheme() . '://' . $uri->getAuthority();
$pathInfo = $uri->getPath();

$question = $baseUrl . $pathInfo === '/' ? '/?' : '?';
$url = $baseUrl . $pathInfo;

return $query ? $url . $question . $query : $url;
}

/**
* Parse the pattern and format for usage.
*/
protected function decodedPath(ServerRequestInterface $request): string
{
$path = trim($request->getUri()->getPath(), '/');

return rawurldecode($path === '' ? '/' : $path);
}
}
180 changes: 180 additions & 0 deletions src/foundation/src/Http/Middleware/VerifyCsrfToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<?php

declare(strict_types=1);

namespace SwooleTW\Hyperf\Foundation\Http\Middleware;

use Hyperf\Collection\Arr;
use Hyperf\Contract\ConfigInterface;
use Hyperf\HttpServer\Request;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use SwooleTW\Hyperf\Cookie\Cookie;
use SwooleTW\Hyperf\Encryption\Contracts\Encrypter;
use SwooleTW\Hyperf\Foundation\Contracts\Application as ApplicationContract;
use SwooleTW\Hyperf\Foundation\Http\Middleware\Concerns\ExcludesPaths;
use SwooleTW\Hyperf\Session\Contracts\Session as SessionContract;
use SwooleTW\Hyperf\Session\TokenMismatchException;
use SwooleTW\Hyperf\Support\Traits\InteractsWithTime;

class VerifyCsrfToken implements MiddlewareInterface
{
use InteractsWithTime;
use ExcludesPaths;

/**
* The URIs that should be excluded.
*
* @var array<int, string>
*/
protected array $except = [];

/**
* The globally ignored URIs that should be excluded from CSRF verification.
*/
protected static array $neverVerify = [];

/**
* Indicates whether the XSRF-TOKEN cookie should be set on the response.
*/
protected bool $addHttpCookie = true;

/**
* Create a new middleware instance.
*/
public function __construct(
protected ContainerInterface $app,
protected ConfigInterface $config,
protected Encrypter $encrypter,
protected Request $request
) {
}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ($this->isReading($request)
|| $this->runningUnitTests()
|| $this->inExceptArray($request)
|| $this->tokensMatch()
) {
$response = $handler->handle($request);
if ($this->shouldAddXsrfTokenCookie()) {
$response = $this->addCookieToResponse($response);
}

return $response;
}

throw new TokenMismatchException('CSRF token mismatch.');
}

/**
* Determine if the HTTP request uses a ‘read’ verb.
*/
protected function isReading(ServerRequestInterface $request): bool
{
return in_array($request->getMethod(), ['HEAD', 'GET', 'OPTIONS']);
}

/**
* Determine if the application is running unit tests.
*/
protected function runningUnitTests(): bool
{
if (! $this->app instanceof ApplicationContract) {
return false;
}

return $this->app->runningUnitTests();
}

/**
* Get the URIs that should be excluded.
*/
public function getExcludedPaths(): array
{
return array_merge($this->except, static::$neverVerify);
}

/**
* Determine if the session and input CSRF tokens match.
*/
protected function tokensMatch(): bool
{
$token = $this->getTokenFromRequest();
$sessionToken = $this->app->get(SessionContract::class)->token();

return is_string($sessionToken)
&& is_string($token)
&& hash_equals($sessionToken, $token);
}

/**
* Get the CSRF token from the request.
*/
protected function getTokenFromRequest(): ?string
{
return $this->request->input('_token')
?? $this->request->header('X-CSRF-TOKEN')
?? null;
}

/**
* Determine if the cookie should be added to the response.
*/
public function shouldAddXsrfTokenCookie(): bool
{
return $this->addHttpCookie;
}

/**
* Add the CSRF token to the response cookies.
*/
protected function addCookieToResponse(ResponseInterface $response): ResponseInterface
{
/* @phpstan-ignore-next-line */
return $response->withCookie(
$this->newCookie($this->config->get('session', []))
);
}

/**
* Create a new "XSRF-TOKEN" cookie that contains the CSRF token.
*/
protected function newCookie(array $config): Cookie
{
return new Cookie(
'XSRF-TOKEN',
$this->app->get(SessionContract::class)->token(),
$this->availableAt(60 * $config['lifetime']),
$config['path'] ?? '/',
$config['domain'] ?? '',
$config['secure'] ?? false,
false,
false,
$config['same_site'] ?? null,
$config['partitioned'] ?? false
);
}

/**
* Indicate that the given URIs should be excluded from CSRF verification.
*/
public static function except(array|string $uris): void
{
static::$neverVerify = array_values(array_unique(
array_merge(static::$neverVerify, Arr::wrap($uris))
));
}

/**
* Flush the state of the middleware.
*/
public static function flushState(): void
{
static::$neverVerify = [];
}
}
39 changes: 26 additions & 13 deletions src/foundation/src/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use Carbon\Carbon;
use Hyperf\Context\ApplicationContext;
use Hyperf\Contract\Arrayable;
use Hyperf\Contract\SessionInterface;
use Hyperf\Contract\ValidatorInterface;
use Hyperf\HttpMessage\Cookie\Cookie;
use Hyperf\Stringable\Stringable;
Expand All @@ -26,7 +25,9 @@
use SwooleTW\Hyperf\HttpMessage\Exceptions\HttpResponseException;
use SwooleTW\Hyperf\HttpMessage\Exceptions\NotFoundHttpException;
use SwooleTW\Hyperf\Router\UrlGenerator;
use SwooleTW\Hyperf\Session\Contracts\Session as SessionContract;
use SwooleTW\Hyperf\Support\Contracts\Responsable;
use SwooleTW\Hyperf\Support\HtmlString;

use function SwooleTW\Hyperf\Filesystem\join_paths;

Expand Down Expand Up @@ -262,6 +263,28 @@ function cookie(string $name, string $value, int $minutes = 0, string $path = ''
}
}

if (! function_exists('csrf_token')) {
/**
* Get the CSRF token value.
*
* @throws \RuntimeException
*/
function csrf_token(): string
{
return \SwooleTW\Hyperf\Session\csrf_token();
}
}

if (! function_exists('csrf_field')) {
/**
* Generate a CSRF token form field.
*/
function csrf_field(): HtmlString
{
return \SwooleTW\Hyperf\Session\csrf_field();
}
}

if (! function_exists('app')) {
/**
* Get the available container instance.
Expand Down Expand Up @@ -523,21 +546,11 @@ function rescue(callable $callback, $rescue = null, $report = true)
*
* If an array is passed as the key, we will assume you want to set an array of values.
*
* @return mixed|SessionInterface
* @return mixed|SessionContract
*/
function session(null|array|string $key = null, mixed $default = null): mixed
{
$session = app(SessionInterface::class);

if (is_null($key)) {
return $session;
}

if (is_array($key)) {
return $session->put($key);
}

return $session->get($key, $default);
return \SwooleTW\Hyperf\Session\session($key, $default);
}
}

Expand Down
Loading

0 comments on commit a047f4e

Please sign in to comment.