Issues (6)

src/ServerMiddleware.php (2 issues)

1
<?php declare(strict_types=1);
2
3
namespace Jasny\HttpDigest;
4
5
use Psr\Http\Message\ServerRequestInterface as ServerRequest;
6
use Psr\Http\Message\ResponseInterface as Response;
7
use Psr\Http\Message\ResponseFactoryInterface as ResponseFactory;
8
use Psr\Http\Server\MiddlewareInterface;
9
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
10
11
/**
12
 * Middleware to verify HTTP Digest header.
13
 * Can be used both as single pass (PSR-15) and double pass middleware.
14
 */
15
class ServerMiddleware implements MiddlewareInterface
16
{
17
    /**
18
     * @var HttpDigest
19
     */
20
    protected $service;
21
22
    /**
23
     * @var ResponseFactory|null
24
     */
25
    protected $responseFactory;
26
27
    /**
28
     * @var bool
29
     */
30
    protected $optional = false;
31
32
    /**
33
     * Class constructor.
34
     *
35
     * @param HttpDigest        $service
36
     * @param ResponseFactory|null $responseFactory
37
     */
38 32
    public function __construct(HttpDigest $service, ?ResponseFactory $responseFactory = null)
39
    {
40 32
        $this->service = $service;
41 32
        $this->responseFactory = $responseFactory;
42 32
    }
43
44
    /**
45
     * Never require a Digest header, even for POST, etc requests.
46
     *
47
     * @param bool $optional
48
     * @return static
49
     */
50 4
    public function withOptionalDigest(bool $optional = true)
51
    {
52 4
        if ($this->optional === $optional) {
53 1
            return $this;
54
        }
55
56 4
        $clone = clone $this;
57 4
        $clone->optional = $optional;
58
59 4
        return $clone;
60
    }
61
62
    /**
63
     * Process an incoming server request (PSR-15).
64
     *
65
     * @param ServerRequest  $request
66
     * @param RequestHandler $handler
67
     * @return Response
68
     * @throws \RuntimeException if unauthorized response can't be created
69
     */
70 19
    public function process(ServerRequest $request, RequestHandler $handler): Response
71
    {
72
        $next = function (ServerRequest $request) use ($handler) {
73 14
            return $handler->handle($request);
74 19
        };
75
76 19
        return $request->hasHeader('Digest')
77 9
            ? $this->handleDigestRequest($request, null, $next)
78 18
            : $this->handleNoDigestRequest($request, null, $next);
79
    }
80
81
    /**
82
     * Get a callback that can be used as double pass middleware.
83
     *
84
     * @return callable
85
     */
86 12
    public function asDoublePass(): callable
87
    {
88
        return function (ServerRequest $request, Response $response, callable $next): Response {
89 12
            return $request->hasHeader('Digest')
90 9
                ? $this->handleDigestRequest($request, $response, $next)
91 12
                : $this->handleNoDigestRequest($request, $response, $next);
92 12
        };
93
    }
94
95
96
    /**
97
     * Check if the should have a digest header.
98
     *
99
     * @param ServerRequest $request
100
     * @return bool
101
     */
102 10
    protected function shouldHaveDigest(ServerRequest $request)
103
    {
104 10
        switch (strtoupper($request->getMethod())) {
105 10
            case 'GET':
106 9
            case 'HEAD':
107 8
            case 'OPTIONS':
108 2
                return false;
109
110 8
            case 'PATCH':
111 6
            case 'POST':
112 4
            case 'PUT':
113 6
                return true;
114
115
            default:
116 2
                return $request->getBody()->getSize() > 0;
117
        }
118
    }
119
120
    /**
121
     * Handle request with a Digest header.
122
     *
123
     * @param ServerRequest  $request
124
     * @param Response|null  $response
125
     * @param callable       $next
126
     * @return Response
127
     * @throws \RuntimeException when the bad request response can't be created.
128
     */
129 18
    protected function handleDigestRequest(ServerRequest $request, ?Response $response, callable $next): Response
130
    {
131
        try {
132 18
            $this->service->verify($request->getBody()->getContents(), $request->getHeaderLine('Digest'));
133 4
        } catch (HttpDigestException $exception) {
134 4
            return $this->createBadRequestResponse($response, $exception->getMessage());
135
        }
136
137 14
        return $next($request, $response);
138
    }
139
140
    /**
141
     * Handle request without a Digest header.
142
     *
143
     * @param ServerRequest  $request
144
     * @param Response|null  $response
145
     * @param callable       $next
146
     * @return Response
147
     * @throws \RuntimeException when the bad request response can't be created.
148
     */
149 13
    protected function handleNoDigestRequest(ServerRequest $request, ?Response $response, callable $next): Response
150
    {
151 13
        if (!$this->optional && $this->shouldHaveDigest($request)) {
152 6
            return $this->createBadRequestResponse($response, 'digest header missing');
153
        }
154
155 7
        return $next($request, $response);
156
    }
157
158
    /**
159
     * Create a response using the response factory.
160
     *
161
     * @param int           $status            Response status
162
     * @param Response|null $originalResponse
163
     * @return Response
164
     */
165 10
    protected function createResponse(int $status, ?Response $originalResponse = null): Response
166
    {
167 10
        if ($this->responseFactory === null && $originalResponse === null) {
168 1
            throw new \BadMethodCallException('Response factory not set');
169
        }
170
171 9
        return $this->responseFactory !== null
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->responseFa...nalResponse->getBody()) could return the type null which is incompatible with the type-hinted return Psr\Http\Message\ResponseInterface. Consider adding an additional type-check to rule them out.
Loading history...
172 5
            ? $this->responseFactory->createResponse($status)
173 9
            : $originalResponse->withStatus($status)->withBody(clone $originalResponse->getBody());
0 ignored issues
show
The method withStatus() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

173
            : $originalResponse->/** @scrutinizer ignore-call */ withStatus($status)->withBody(clone $originalResponse->getBody());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
174
    }
175
176
    /**
177
     * Create a `400 Bad Request` response.
178
     *
179
     * @param Response|null $response
180
     * @param string        $message
181
     * @return Response
182
     * @throws \RuntimeException when can't write body.
183
     */
184 10
    protected function createBadRequestResponse(?Response $response, string $message): Response
185
    {
186 10
        $errorResponse = $this->createResponse(400, $response)
187 9
            ->withHeader('Want-Digest', $this->service->getWantDigest())
188 9
            ->withHeader('Content-Type', 'text/plain');
189
190 9
        $errorResponse->getBody()->write($message);
191
192 9
        return $errorResponse;
193
    }
194
}
195