Completed
Push — master ( e93629...dcd410 )
by
unknown
19s queued 15s
created

PageViewProxy::handleGet()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 47
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 24
c 0
b 0
f 0
nc 4
nop 1
dl 0
loc 47
rs 9.536
1
<?php
2
3
/**
4
 * (c) Kitodo. Key to digital objects e.V. <[email protected]>
5
 *
6
 * This file is part of the Kitodo and TYPO3 projects.
7
 *
8
 * @license GNU General Public License version 3 or later.
9
 * For the full copyright and license information, please read the
10
 * LICENSE.txt file that was distributed with this source code.
11
 */
12
13
namespace Kitodo\Dlf\Eid;
14
15
use Kitodo\Dlf\Common\Helper;
16
use Kitodo\Dlf\Common\StdOutStream;
17
use Psr\Http\Message\ResponseInterface;
18
use Psr\Http\Message\ServerRequestInterface;
19
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
20
use TYPO3\CMS\Core\Http\JsonResponse;
21
use TYPO3\CMS\Core\Http\RequestFactory;
22
use TYPO3\CMS\Core\Http\Response;
23
use TYPO3\CMS\Core\Utility\GeneralUtility;
24
25
/**
26
 * eID image proxy for plugin 'Page View' of the 'dlf' extension
27
 *
28
 * Supported query parameters:
29
 * - `url` (mandatory): The URL to be proxied
30
 * - `uHash` (mandatory): HMAC of the URL
31
 *
32
 * @author Alexander Bigga <[email protected]>
33
 * @package TYPO3
34
 * @subpackage dlf
35
 * @access public
36
 */
37
class PageViewProxy
38
{
39
    /**
40
     * @var RequestFactory
41
     */
42
    protected $requestFactory;
43
44
    /**
45
     * @var mixed
46
     */
47
    protected $extConf;
48
49
    public function __construct()
50
    {
51
        $this->requestFactory = GeneralUtility::makeInstance(RequestFactory::class);
52
        $this->extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('dlf');
53
    }
54
55
    /**
56
     * Return a response that is derived from $response and contains CORS
57
     * headers to be sent to the client.
58
     *
59
     * @return ResponseInterface $response
60
     * @return ServerRequestInterface $request The incoming request.
61
     * @return ResponseInterface
62
     */
63
    protected function withCorsResponseHeaders(
64
        ResponseInterface $response,
65
        ServerRequestInterface $request
66
    ): ResponseInterface {
67
        $origin = (string) ($request->getHeaderLine('Origin') ? : '*');
68
69
        return $response
70
            ->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')
71
            ->withHeader('Access-Control-Allow-Origin', $origin)
72
            ->withHeader('Access-Control-Max-Age', '86400');
73
    }
74
75
    /**
76
     * Takes headers listed in $headerNames from $fromResponse, adds them to
77
     * $toResponse and returns the result.
78
     *
79
     * @param ResponseInterface $fromResponse
80
     * @param ResponseInterface $toResponse
81
     * @param array $headerNames
82
     * @return ResponseInterface
83
     */
84
    protected function copyHeaders(
85
        ResponseInterface $fromResponse,
86
        ResponseInterface $toResponse,
87
        array $headerNames
88
    ) {
89
        $result = $toResponse;
90
91
        foreach ($headerNames as $headerName) {
92
            $headerValues = $fromResponse->getHeader($headerName);
93
            // Don't include empty header field when not present
94
            if (!empty($headerValues)) {
95
                $result = $result->withAddedHeader($headerName, $headerValues);
96
            }
97
        }
98
99
        return $result;
100
    }
101
102
    /**
103
     * Handle an OPTIONS request.
104
     *
105
     * @param ServerRequestInterface $request
106
     * @return ResponseInterface
107
     */
108
    protected function handleOptions(ServerRequestInterface $request): ResponseInterface
109
    {
110
        // 204 No Content
111
        $response = GeneralUtility::makeInstance(Response::class)
112
            ->withStatus(204);
113
        return $this->withCorsResponseHeaders($response, $request);
114
    }
115
116
    /**
117
     * Handle a GET request.
118
     *
119
     * @param ServerRequestInterface $request
120
     * @return ResponseInterface
121
     */
122
    protected function handleGet(ServerRequestInterface $request): ResponseInterface
123
    {
124
        $queryParams = $request->getQueryParams();
125
126
        $url = (string) ($queryParams['url'] ?? '');
127
        if (!Helper::isValidHttpUrl($url)) {
128
            return new JsonResponse(['message' => 'Did not receive a valid URL.'], 400);
129
        }
130
131
        // get and verify the uHash
132
        $uHash = (string) ($queryParams['uHash'] ?? '');
133
        if (!hash_equals(GeneralUtility::hmac($url, 'PageViewProxy'), $uHash)) {
134
            return new JsonResponse(['message' => 'No valid uHash passed!'], 401);
135
        }
136
137
        try {
138
            $targetResponse = $this->requestFactory->request($url, 'GET', [
139
                'headers' => [
140
                    'User-Agent' => $this->extConf['useragent'] ?? 'Kitodo.Presentation Proxy',
141
                ],
142
143
                // For performance, don't download content up-front. Rather, we'll
144
                // download and upload simultaneously.
145
                // https://docs.guzzlephp.org/en/6.5/request-options.html#stream
146
                'stream' => true,
147
148
                // Don't throw exceptions when a non-success status code is
149
                // received. We handle these manually.
150
                'http_errors' => false,
151
            ]);
152
        } catch (\Exception $e) {
153
            return new JsonResponse(['message' => 'Could not fetch resource of given URL.'], 500);
154
        }
155
156
        $body = new StdOutStream($targetResponse->getBody());
157
158
        $clientResponse = GeneralUtility::makeInstance(Response::class)
159
            ->withStatus($targetResponse->getStatusCode())
160
            ->withBody($body);
161
162
        $clientResponse = $this->copyHeaders($targetResponse, $clientResponse, [
163
            'Content-Length',
164
            'Content-Type',
165
            'Last-Modified',
166
        ]);
167
168
        return $this->withCorsResponseHeaders($clientResponse, $request);
169
    }
170
171
    /**
172
     * The main method of the eID script
173
     *
174
     * @access public
175
     *
176
     * @param ServerRequestInterface $request
177
     * @return ResponseInterface
178
     */
179
    public function main(ServerRequestInterface $request)
180
    {
181
        switch ($request->getMethod()) {
182
            case 'OPTIONS':
183
                return $this->handleOptions($request);
184
185
            case 'GET':
186
                return $this->handleGet($request);
187
188
            default:
189
                // 405 Method Not Allowed
190
                return GeneralUtility::makeInstance(Response::class)
191
                    ->withStatus(405);
192
        }
193
    }
194
}
195