From ed8f16e98d8dfd1ee6d15ba53d460a5074169362 Mon Sep 17 00:00:00 2001 From: Laurent Constantin Date: Sun, 25 Aug 2024 11:32:00 +0200 Subject: [PATCH 1/2] fix(kobo): Proxy uses cookies and https --- src/Kobo/Proxy/KoboStoreProxy.php | 51 ++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/src/Kobo/Proxy/KoboStoreProxy.php b/src/Kobo/Proxy/KoboStoreProxy.php index 55a1bfbe..e7e97318 100644 --- a/src/Kobo/Proxy/KoboStoreProxy.php +++ b/src/Kobo/Proxy/KoboStoreProxy.php @@ -4,6 +4,7 @@ use App\Security\KoboTokenExtractor; use GuzzleHttp\Client; +use GuzzleHttp\Cookie\CookieJar; use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Promise\PromiseInterface; use Nyholm\Psr7\Factory\Psr17Factory; @@ -90,9 +91,10 @@ private function _proxy(Request $request, string $hostname, array $config = []): $psrRequest = $this->convertRequest($request, $hostname); $accessToken = $this->tokenExtractor->extractAccessToken($request) ?? 'unknown'; - + $jar = CookieJar::fromArray($psrRequest->getCookieParams(), $psrRequest->getUri()->getHost()); $client = new Client(); $psrResponse = $client->send($psrRequest, [ + 'cookies' => $jar, 'base_uri' => $hostname, 'handler' => $this->koboProxyLoggerFactory->createStack($accessToken), 'http_errors' => false, @@ -108,17 +110,21 @@ protected function getTransformedUrl(Request $request): UriInterface $psrRequest = $this->toPsrRequest($request); $hostname = $this->configuration->isImageHostUrl($psrRequest) ? $this->configuration->getImageApiUrl() : $this->configuration->getStoreApiUrl(); - return $this->transformUrl($psrRequest, $hostname); + return $this->transformUrl($psrRequest, $hostname, $request->getScheme()); } - private function transformUrl(ServerRequestInterface $psrRequest, string $hostname): UriInterface + private function transformUrl(ServerRequestInterface $psrRequest, string $hostname, string $scheme): UriInterface { $host = parse_url($hostname, PHP_URL_HOST); $host = $host === false ? $hostname : $host; $host = $host ?? $hostname; $path = $this->tokenExtractor->getOriginalPath($psrRequest, $psrRequest->getUri()->getPath()); - return $psrRequest->getUri()->withHost($host)->withPath($path); + return $psrRequest->getUri() + ->withHost($host) + ->withPath($path) + ->withScheme($scheme) + ; } private function toPsrRequest(Request $request): ServerRequestInterface @@ -126,7 +132,37 @@ private function toPsrRequest(Request $request): ServerRequestInterface $psr17Factory = new Psr17Factory(); $psrHttpFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory); - return $psrHttpFactory->createRequest($request); + $request = clone $request; + + // Remove server attributes + foreach ($request->server->all() as $key => $value) { + if ($key !== 'HTTPS' && $key !== 'X-Forwarded-Proto') { + $request->server->remove($key); + } + } + + // Remove route attributes + foreach ($request->attributes->all() as $key => $value) { + $request->attributes->remove($key); + } + + // Remove cookies + foreach ($request->cookies->all() as $key => $value) { + if (in_array($key, ['PHPSESSID', 'XDEBUG_SESSION'], true)) { + $request->cookies->remove($key); + } + } + + // Remove headers + foreach ($request->headers->all() as $key => $value) { + if (str_starts_with($key, 'x-forwarded-') || $key === 'x-real-ip' || $key === 'cookie') { + $request->headers->remove($key); + } + } + + return $psrHttpFactory->createRequest($request) + ->withCookieParams($request->cookies->all()) + ; } public function proxyAsync(Request $request, bool $streamAllowed): PromiseInterface @@ -137,8 +173,10 @@ public function proxyAsync(Request $request, bool $streamAllowed): PromiseInterf $accessToken = $this->tokenExtractor->extractAccessToken($request) ?? 'unknown'; $client = new Client(); + $jar = CookieJar::fromArray($psrRequest->getCookieParams(), $psrRequest->getUri()->getHost()); return $client->sendAsync($psrRequest, [ + 'cookies' => $jar, 'base_uri' => $hostname, 'handler' => $this->koboProxyLoggerFactory->createStack($accessToken), 'http_errors' => false, @@ -154,11 +192,10 @@ private function convertRequest(Request $request, string $hostname): RequestInte $host = parse_url($hostname, PHP_URL_HOST); $host = $host === false ? $hostname : $host; $request->headers->set('Host', $host); - $request->server->set('HTTPS', 'on'); // Force HTTPS (for cli) $psrRequest = $this->toPsrRequest($request); $psrRequest = $this->cleanup($psrRequest); - $url = $this->transformUrl($psrRequest, $hostname); + $url = $this->transformUrl($psrRequest, $hostname, $request->getScheme()); return $psrRequest->withUri($url); } From 8bb905a59ea2ded4d40b197160c28e04dbf658ec Mon Sep 17 00:00:00 2001 From: Laurent Constantin Date: Sun, 25 Aug 2024 12:23:02 +0200 Subject: [PATCH 2/2] feat(kobo): Proxy sync request --- .../Kobo/KoboAuthDeviceController.php | 42 +++++++++++++++++ src/Controller/Kobo/KoboController.php | 3 -- src/Controller/Kobo/KoboSyncController.php | 4 ++ .../Kobo/KoboUserProfileController.php | 47 +++++++++++++++++++ src/Kobo/Proxy/KoboProxyLogger.php | 4 ++ src/Kobo/Proxy/KoboStoreProxy.php | 11 ++++- 6 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 src/Controller/Kobo/KoboAuthDeviceController.php create mode 100644 src/Controller/Kobo/KoboUserProfileController.php diff --git a/src/Controller/Kobo/KoboAuthDeviceController.php b/src/Controller/Kobo/KoboAuthDeviceController.php new file mode 100644 index 00000000..24036639 --- /dev/null +++ b/src/Controller/Kobo/KoboAuthDeviceController.php @@ -0,0 +1,42 @@ +koboProxyConfiguration->useProxy()) { + return $this->koboStoreProxy->proxy( + $request, ['stream' => true] + ); + } + + $response = new Response(); + $response->headers->set('Content-Type', 'application/json'); + + return $response; + } +} diff --git a/src/Controller/Kobo/KoboController.php b/src/Controller/Kobo/KoboController.php index 1f0928e3..1f5a0f4f 100644 --- a/src/Controller/Kobo/KoboController.php +++ b/src/Controller/Kobo/KoboController.php @@ -42,10 +42,7 @@ public function index(): Response #[Route('/v1/products/{uuid}/prices', requirements: ['uuid' => '^[a-zA-Z0-9\-]+$'], methods: ['GET', 'POST'])] #[Route('/v1/products/{uuid}/recommendations', requirements: ['uuid' => '^[a-zA-Z0-9\-]+$'], methods: ['GET', 'POST'])] #[Route('/v1/products/{uuid}/reviews', requirements: ['uuid' => '^[a-zA-Z0-9\-]+$'], methods: ['GET', 'POST'])] - #[Route('/v1/user/profile')] #[Route('/v1/configuration')] - #[Route('/v1/auth/device')] - #[Route('/v1/auth/refresh')] #[Route('/v1/library/borrow')] #[Route('/v1/auth/exchange')] #[Route('/v1/library/{uuid}', methods: ['DELETE'])] diff --git a/src/Controller/Kobo/KoboSyncController.php b/src/Controller/Kobo/KoboSyncController.php index b92ddbd4..ae11e1dc 100644 --- a/src/Controller/Kobo/KoboSyncController.php +++ b/src/Controller/Kobo/KoboSyncController.php @@ -49,6 +49,10 @@ public function __construct( #[Route('/v1/library/sync', name: 'api_endpoint_v1_library_sync')] public function apiEndpoint(KoboDevice $kobo, SyncToken $syncToken, Request $request): Response { + // if (true) { + // return $this->koboStoreProxy->proxy($request); + // } + $forced = $kobo->isForceSync() || $request->query->has('force'); $count = $this->koboSyncedBookRepository->countByKoboDevice($kobo); if ($forced || $count === 0) { diff --git a/src/Controller/Kobo/KoboUserProfileController.php b/src/Controller/Kobo/KoboUserProfileController.php new file mode 100644 index 00000000..b282e062 --- /dev/null +++ b/src/Controller/Kobo/KoboUserProfileController.php @@ -0,0 +1,47 @@ +koboProxyConfiguration->useProxy()) { + return $this->koboStoreProxy->proxy($request); + } + + $tokenParts = []; + $tokenParts[] = base64_encode((string) json_encode([ + 'typ' => 1, + 'ver' => 'v1', + 'ptyp' => 'ApiUserToken', + ])); + $tokenParts[] = base64_encode((string) json_encode([ + 'LoyaltyMembershipVersion' => 2147483647, + 'LastModifiedTime' => -62135596800, + 'BuildVersion' => '1.0.0', + 'LifetimeTagsHash' => -790277027, + ])); + + $response = new Response(); + $response->headers->set('Content-Type', 'application/json'); + $response->headers->set('X-Kobo-Apitoken', base64_encode((string) json_encode(['x-kobo-profile-token' => implode('.', $tokenParts)]))); + + return $response; + } +} diff --git a/src/Kobo/Proxy/KoboProxyLogger.php b/src/Kobo/Proxy/KoboProxyLogger.php index 02ac2272..4b253420 100644 --- a/src/Kobo/Proxy/KoboProxyLogger.php +++ b/src/Kobo/Proxy/KoboProxyLogger.php @@ -54,10 +54,14 @@ protected function onFailure(RequestInterface $request): \Closure private function log(RequestInterface $request, ?ResponseInterface $response = null, ?\Throwable $error = null): void { + $body = $response?->getBody()->__toString(); + $response?->getBody()->rewind(); + $this->logger->info(sprintf('Proxied: %s', (string) $request->getUri()), [ 'method' => $request->getMethod(), 'status' => $response?->getStatusCode(), 'token_hash' => md5($this->accessToken), + 'body' => $body, ]); if ($error instanceof \Throwable) { diff --git a/src/Kobo/Proxy/KoboStoreProxy.php b/src/Kobo/Proxy/KoboStoreProxy.php index e7e97318..5666a2b7 100644 --- a/src/Kobo/Proxy/KoboStoreProxy.php +++ b/src/Kobo/Proxy/KoboStoreProxy.php @@ -91,7 +91,11 @@ private function _proxy(Request $request, string $hostname, array $config = []): $psrRequest = $this->convertRequest($request, $hostname); $accessToken = $this->tokenExtractor->extractAccessToken($request) ?? 'unknown'; - $jar = CookieJar::fromArray($psrRequest->getCookieParams(), $psrRequest->getUri()->getHost()); + $jar = new CookieJar(); + if ($psrRequest instanceof ServerRequestInterface) { + $jar = CookieJar::fromArray($psrRequest->getCookieParams(), $psrRequest->getUri()->getHost()); + } + $client = new Client(); $psrResponse = $client->send($psrRequest, [ 'cookies' => $jar, @@ -173,7 +177,10 @@ public function proxyAsync(Request $request, bool $streamAllowed): PromiseInterf $accessToken = $this->tokenExtractor->extractAccessToken($request) ?? 'unknown'; $client = new Client(); - $jar = CookieJar::fromArray($psrRequest->getCookieParams(), $psrRequest->getUri()->getHost()); + $jar = new CookieJar(); + if ($psrRequest instanceof ServerRequestInterface) { + $jar = CookieJar::fromArray($psrRequest->getCookieParams(), $psrRequest->getUri()->getHost()); + } return $client->sendAsync($psrRequest, [ 'cookies' => $jar,