diff --git a/Dockerfile b/Dockerfile index 6d20afb5..c155d53f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,6 +17,7 @@ COPY . /app RUN echo '* * * * * /usr/bin/run-app-cron'>>/etc/crontabs/www-data && \ cp /app/docker/files/nginx.conf /etc/nginx/nginx.conf && \ + cp /app/docker/files/fpm.conf /usr/local/etc/php-fpm.d/docker.conf && \ cp /app/docker/files/entrypoint.sh /usr/bin/entrypoint-docker && \ cp /app/docker/files/app_console.sh /usr/bin/console && \ cp /app/docker/files/cron.sh /usr/bin/run-app-cron && \ diff --git a/docker/files/fpm.conf b/docker/files/fpm.conf new file mode 100644 index 00000000..04fb52b8 --- /dev/null +++ b/docker/files/fpm.conf @@ -0,0 +1,8 @@ +[global] +error_log = /proc/self/fd/2 +log_limit = 8192 + +[www] +clear_env = no +catch_workers_output = yes +decorate_workers_output = no diff --git a/docker/files/nginx.conf b/docker/files/nginx.conf index eb391f64..f97f1075 100644 --- a/docker/files/nginx.conf +++ b/docker/files/nginx.conf @@ -23,7 +23,8 @@ http { '' close; } - log_format traceable 'response_status: $status - response_text: "$upstream_http_x_status" - request_key: "$http_x_apikey" ' + log_format traceable '[$time_iso8601] response_status: $status - response_text: "$upstream_http_x_status" - request_key: "$http_x_apikey" ' + '- webhook_event: "$upstream_http_x_wh_event" - webhook_event: "$upstream_http_x_wh_type" ' '- http_request: "$request":$body_bytes_sent - user_ip: $remote_addr - http_host: $http_host - user_agent: "$http_user_agent"'; server { diff --git a/public/index.php b/public/index.php index 97ad6ae2..7ddc885a 100644 --- a/public/index.php +++ b/public/index.php @@ -2,8 +2,6 @@ declare(strict_types=1); -use Psr\Http\Message\ServerRequestInterface; - error_reporting(E_ALL); ini_set('error_reporting', 'On'); ini_set('display_errors', 'Off'); @@ -46,4 +44,4 @@ set_exception_handler(function (Throwable $e) { exit(1); }); -(new App\Libs\Initializer())->boot()->runHttp(fn(ServerRequestInterface $request) => serveHttpRequest($request)); +(new App\Libs\Initializer())->boot()->runHttp(); diff --git a/src/Libs/Initializer.php b/src/Libs/Initializer.php index 8d43f969..a0ae00f7 100644 --- a/src/Libs/Initializer.php +++ b/src/Libs/Initializer.php @@ -6,6 +6,7 @@ namespace App\Libs; use App\Cli; use App\Libs\Extends\ConsoleOutput; +use App\Libs\Storage\StorageInterface; use Closure; use Laminas\HttpHandlerRunner\Emitter\EmitterInterface; use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; @@ -123,12 +124,14 @@ final class Initializer /** * Handle HTTP Request. * - * @param Closure(ServerRequestInterface): ResponseInterface $fn + * @param ServerRequestInterface|null $request + * @param EmitterInterface|null $emitter + * @param null|Closure(ServerRequestInterface): ResponseInterface $fn */ public function runHttp( - Closure $fn, ServerRequestInterface|null $request = null, - EmitterInterface|null $emitter = null + EmitterInterface|null $emitter = null, + Closure|null $fn = null, ): void { $emitter = $emitter ?? new SapiEmitter(); @@ -138,7 +141,7 @@ final class Initializer } try { - $response = $fn($request); + $response = null === $fn ? $this->defaultHttpServer($request) : $fn($request); } catch (Throwable $e) { Container::get(LoggerInterface::class)->error( $e->getMessage(), @@ -153,6 +156,217 @@ final class Initializer $emitter->emit($response); } + private function defaultHttpServer(ServerRequestInterface $request): ResponseInterface + { + $log = []; + $logger = Container::get(LoggerInterface::class); + + try { + if (true === (bool)env('WS_REQUEST_DEBUG') || null !== ag($request->getQueryParams(), 'rdebug')) { + saveRequestPayload($request); + } + + $request = preServeHttpRequest($request); + + // -- get apikey from header or query. + $apikey = $request->getHeaderLine('x-apikey'); + if (empty($apikey)) { + $apikey = ag($request->getQueryParams(), 'apikey', ''); + if (empty($apikey)) { + $log[] = 'No api key in headers or query'; + throw new HttpException('No API key was given.', 400); + } + } + + $server = []; + Config::get('servers', []); + + $validUser = $validUUid = null; + + // -- Find Server + foreach (Config::get('servers', []) as $name => $info) { + if (null === ag($info, 'webhook.token')) { + continue; + } + + if (!hash_equals(ag($info, 'webhook.token'), $apikey)) { + continue; + } + + $userId = ag($info, 'user', null); + + if (true === (true === ag($info, 'webhook.match.user') && null !== $userId)) { + if (null === ($requestUser = $request->getAttribute('USER_ID', null))) { + $validUser = false; + $log[] = 'Request user is not set'; + continue; + } + if ((string)$userId !== (string)$requestUser) { + $validUser = false; + $log[] = sprintf( + 'Request user [%s] does not match config user [%s]', + $requestUser ?? 'NO USER_ID', + $userId + ); + continue; + } + $validUser = true; + } + + $uuid = ag($info, 'uuid', null); + + if (true === (true === ag($info, 'webhook.match.uuid') && null !== $uuid)) { + if (null === ($requestServerId = $request->getAttribute('SERVER_ID', null))) { + $validUUid = false; + $log[] = 'Request server unique id is not set'; + continue; + } + + if ((string)$uuid !== (string)$requestServerId) { + $validUUid = false; + $log[] = sprintf( + 'Request UUID [%s] does not match config UUID [%s]', + $requestServerId ?? 'NO SERVER_ID', + $uuid + ); + continue; + } + + $validUUid = true; + } + + $server = array_replace_recursive(['name' => $name], $info); + break; + } + + if (empty($server)) { + if (false === $validUser) { + $message = 'API key is valid, User checks failed.'; + } elseif (false === $validUUid) { + $message = 'API key and user check is valid, Server unique id checks failed.'; + } else { + $message = 'Invalid API key was given.'; + } + throw new HttpException($message, 401); + } + + if (true !== ag($server, 'webhook.import')) { + $log[] = 'Import disabled for this server'; + throw new HttpException( + sprintf( + 'Import via webhook for this server \'%s\' is disabled.', + ag($server, 'name') + ), + 500 + ); + } + + try { + $server['class'] = makeServer($server, $server['name']); + } catch (RuntimeException $e) { + $log[] = 'Creating Instance of the Backend has failed.'; + throw new HttpException($e->getMessage(), 500); + } + + $entity = $server['class']->parseWebhook($request); + + if (!$entity->hasGuids()) { + return new Response(status: 204, headers: [ + 'X-Status' => 'No GUIDs.', + 'X-WH-Type' => $request->getAttribute('WH_TYPE', 'not_set'), + 'X-WH-Event' => $request->getAttribute('WH_EVENT', 'not_set'), + ]); + } + + $storage = Container::get(StorageInterface::class); + + if (null === ($backend = $storage->get($entity))) { + $entity = $storage->insert($entity); + queuePush($entity); + return jsonResponse(status: 200, body: $entity->getAll(), headers: [ + 'X-Status' => 'Added new entity.', + 'X-WH-Type' => $request->getAttribute('WH_TYPE', 'not_set'), + 'X-WH-Event' => $request->getAttribute('WH_EVENT', 'not_set'), + ]); + } + + if (true === $entity->isTainted()) { + if ($backend->apply($entity, guidOnly: true)->isChanged()) { + if (!empty($entity->meta)) { + $backend->meta = $entity->meta; + } + $backend = $storage->update($backend); + return jsonResponse(status: 200, body: $backend->getAll(), headers: [ + 'X-Status' => 'Event is tainted. Only GUIDs updated.', + 'X-WH-Type' => $request->getAttribute('WH_TYPE', 'not_set'), + 'X-WH-Event' => $request->getAttribute('WH_EVENT', 'not_set'), + ]); + } + + return new Response(status: 200, headers: [ + 'X-Status' => 'Nothing updated, entity state is tainted.', + 'X-WH-Type' => $request->getAttribute('WH_TYPE', 'not_set'), + 'X-WH-Event' => $request->getAttribute('WH_EVENT', 'not_set'), + ]); + } + + if ($backend->updated > $entity->updated) { + if ($backend->apply($entity, guidOnly: true)->isChanged()) { + if (!empty($entity->meta)) { + $backend->meta = $entity->meta; + } + $backend = $storage->update($backend); + return jsonResponse(status: 200, body: $backend->getAll(), headers: [ + 'X-Status' => 'No watch state updated. Only GUIDs updated.', + 'X-WH-Type' => $request->getAttribute('WH_TYPE', 'not_set'), + 'X-WH-Event' => $request->getAttribute('WH_EVENT', 'not_set'), + ]); + } + + return new Response(status: 200, headers: [ + 'X-Status' => 'Entity date is older than what available in storage.', + 'X-WH-Type' => $request->getAttribute('WH_TYPE', 'not_set'), + 'X-WH-Event' => $request->getAttribute('WH_EVENT', 'not_set'), + ]); + } + + if ($backend->apply($entity)->isChanged()) { + $backend = $storage->update($backend); + + queuePush($backend); + + return jsonResponse(status: 200, body: $backend->getAll(), headers: [ + 'X-Status' => 'Item Queued.', + 'X-WH-Type' => $request->getAttribute('WH_TYPE', 'not_set'), + 'X-WH-Event' => $request->getAttribute('WH_EVENT', 'not_set'), + ]); + } + + return new Response(status: 200, headers: ['X-Status' => 'Entity is unchanged.']); + } catch (HttpException $e) { + if (200 === $e->getCode()) { + return new Response(status: $e->getCode(), headers: [ + 'X-Status' => $e->getMessage(), + 'X-WH-Type' => $request->getAttribute('WH_TYPE', 'not_set'), + 'X-WH-Event' => $request->getAttribute('WH_EVENT', 'not_set'), + ]); + } + + $logger->error($e->getMessage(), [ + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'attributes' => $request->getAttributes(), + 'log' => $log, + ]); + + return jsonResponse($e->getCode(), ['error' => true, 'message' => $e->getMessage()], [ + 'X-Status' => $e->getMessage(), + 'X-WH-Type' => $request->getAttribute('WH_TYPE', 'not_set'), + 'X-WH-Event' => $request->getAttribute('WH_EVENT', 'not_set'), + ]); + } + } + private function createDirectories(): void { $dirList = __DIR__ . '/../../config/directories.php'; diff --git a/src/Libs/Servers/EmbyServer.php b/src/Libs/Servers/EmbyServer.php index 3ea39de7..4634a6a6 100644 --- a/src/Libs/Servers/EmbyServer.php +++ b/src/Libs/Servers/EmbyServer.php @@ -70,6 +70,8 @@ class EmbyServer extends JellyfinServer 'SERVER_VERSION' => afterLast($userAgent, '/'), 'USER_ID' => ag($json, 'User.Id', ''), 'USER_NAME' => ag($json, 'User.Name', ''), + 'WH_EVENT' => ag($json, 'Event', 'not_set'), + 'WH_TYPE' => ag($json, 'Item.Type', 'not_set'), ]; foreach ($attributes as $key => $val) { diff --git a/src/Libs/Servers/JellyfinServer.php b/src/Libs/Servers/JellyfinServer.php index 47c95247..a99cd657 100644 --- a/src/Libs/Servers/JellyfinServer.php +++ b/src/Libs/Servers/JellyfinServer.php @@ -240,6 +240,8 @@ class JellyfinServer implements ServerInterface 'SERVER_VERSION' => afterLast($userAgent, '/'), 'USER_ID' => ag($json, 'UserId', ''), 'USER_NAME' => ag($json, 'NotificationUsername', ''), + 'WH_EVENT' => ag($json, 'NotificationType', 'not_set'), + 'WH_TYPE' => ag($json, 'ItemType', 'not_set'), ]; foreach ($attributes as $key => $val) { diff --git a/src/Libs/Servers/PlexServer.php b/src/Libs/Servers/PlexServer.php index 579e76d3..24f1e0a7 100644 --- a/src/Libs/Servers/PlexServer.php +++ b/src/Libs/Servers/PlexServer.php @@ -242,6 +242,8 @@ class PlexServer implements ServerInterface 'SERVER_VERSION' => afterLast($userAgent, '/'), 'USER_ID' => ag($json, 'Account.id', ''), 'USER_NAME' => ag($json, 'Account.title', ''), + 'WH_EVENT' => ag($json, 'event', 'not_set'), + 'WH_TYPE' => ag($json, 'Metadata.type', 'not_set'), ]; foreach ($attributes as $key => $val) { diff --git a/src/Libs/helpers.php b/src/Libs/helpers.php index 8f357c68..ceeebbeb 100644 --- a/src/Libs/helpers.php +++ b/src/Libs/helpers.php @@ -6,9 +6,7 @@ use App\Libs\Config; use App\Libs\Container; use App\Libs\Entity\StateInterface; use App\Libs\Extends\Date; -use App\Libs\HttpException; use App\Libs\Servers\ServerInterface; -use App\Libs\Storage\StorageInterface; use Nyholm\Psr7\Response; use Nyholm\Psr7\Uri; use Psr\Http\Message\ResponseInterface; @@ -309,198 +307,6 @@ if (!function_exists('preServeHttpRequest')) { } } -if (!function_exists('serveHttpRequest')) { - function serveHttpRequest(ServerRequestInterface $request): ResponseInterface - { - $log = []; - $logger = Container::get(LoggerInterface::class); - - try { - if (true === (bool)env('WS_REQUEST_DEBUG') || null !== ag($request->getQueryParams(), 'rdebug')) { - saveRequestPayload($request); - } - - $request = preServeHttpRequest($request); - - // -- get apikey from header or query. - $apikey = $request->getHeaderLine('x-apikey'); - if (empty($apikey)) { - $apikey = ag($request->getQueryParams(), 'apikey', ''); - if (empty($apikey)) { - $log[] = 'No api key in headers or query'; - throw new HttpException('No API key was given.', 400); - } - } - - $server = []; - Config::get('servers', []); - - $validUser = $validUUid = null; - - // -- Find Server - foreach (Config::get('servers', []) as $name => $info) { - if (null === ag($info, 'webhook.token')) { - continue; - } - - if (!hash_equals(ag($info, 'webhook.token'), $apikey)) { - continue; - } - - $userId = ag($info, 'user', null); - - if (true === (true === ag($info, 'webhook.match.user') && null !== $userId)) { - if (null === ($requestUser = $request->getAttribute('USER_ID', null))) { - $validUser = false; - $log[] = 'Request user is not set'; - continue; - } - if ((string)$userId !== (string)$requestUser) { - $validUser = false; - $log[] = sprintf('Request user [%s] does not match config user [%s]', $requestUser, $userId); - continue; - } - $validUser = true; - } - - $uuid = ag($info, 'uuid', null); - - if (true === (true === ag($info, 'webhook.match.uuid') && null !== $uuid)) { - if (null === ($requestServerId = $request->getAttribute('SERVER_ID', null))) { - $validUUid = false; - $log[] = 'Request server unique id is not set'; - continue; - } - - if ((string)$uuid !== (string)$requestServerId) { - $validUUid = false; - $log[] = sprintf('Request UUID [%s] does not match config UUID [%s]', $requestServerId, $uuid); - continue; - } - - $validUUid = true; - } - - $server = array_replace_recursive(['name' => $name], $info); - break; - } - - if (empty($server)) { - if (false === $validUser) { - $message = 'API key is valid, User checks failed.'; - } elseif (false === $validUUid) { - $message = 'API key and user check is valid, Server unique id checks failed.'; - } else { - $message = 'Invalid API key was given.'; - } - throw new HttpException($message, 401); - } - - if (true !== ag($server, 'webhook.import')) { - $log[] = 'Import disabled for this server'; - throw new HttpException( - sprintf( - 'Import via webhook for this server \'%s\' is disabled.', - ag($server, 'name') - ), - 500 - ); - } - - try { - $server['class'] = makeServer($server, $server['name']); - } catch (RuntimeException $e) { - $log[] = 'Creating Instance of the Backend has failed.'; - throw new HttpException($e->getMessage(), 500); - } - - $entity = $server['class']->parseWebhook($request); - - if (!$entity->hasGuids()) { - return new Response(status: 204, headers: ['X-Status' => 'No GUIDs.']); - } - - $storage = Container::get(StorageInterface::class); - - if (null === ($backend = $storage->get($entity))) { - $entity = $storage->insert($entity); - queuePush($entity); - return jsonResponse(status: 200, body: $entity->getAll(), headers: [ - 'X-Status' => 'Added new entity.' - ]); - } - - if (true === $entity->isTainted()) { - if ($backend->apply($entity, guidOnly: true)->isChanged()) { - if (!empty($entity->meta)) { - $backend->meta = $entity->meta; - } - $backend = $storage->update($backend); - return jsonResponse(status: 200, body: $backend->getAll(), headers: [ - 'X-Status' => 'Event is tainted. Only GUIDs updated.', - ]); - } - - return new Response( - status: 200, - headers: ['X-Status' => 'Nothing updated, entity state is tainted.'] - ); - } - - if ($backend->updated > $entity->updated) { - if ($backend->apply($entity, guidOnly: true)->isChanged()) { - if (!empty($entity->meta)) { - $backend->meta = $entity->meta; - } - $backend = $storage->update($backend); - return jsonResponse(status: 200, body: $backend->getAll(), headers: [ - 'X-Status' => 'No watch state updated. Only GUIDs updated.', - ]); - } - - return new Response( - status: 200, - headers: ['X-Status' => 'Entity date is older than what available in storage.'] - ); - } - - if ($backend->apply($entity)->isChanged()) { - $backend = $storage->update($backend); - - queuePush($backend); - - return jsonResponse(status: 200, body: $backend->getAll(), headers: [ - 'X-Status' => 'Item Queued.', - ]); - } - - return new Response(status: 200, headers: ['X-Status' => 'Entity is unchanged.']); - } catch (HttpException $e) { - if (200 === $e->getCode()) { - return new Response(status: $e->getCode(), headers: ['X-Status' => $e->getMessage()]); - } - - $logger->error($e->getMessage(), [ - 'file' => $e->getFile(), - 'line' => $e->getLine(), - 'attributes' => $request->getAttributes(), - 'log' => $log, - ]); - - return jsonResponse( - status: $e->getCode(), - body: [ - 'error' => true, - 'message' => $e->getMessage() - ], - headers: [ - 'X-Status' => $e->getMessage(), - ] - ); - } - } -} - if (!function_exists('queuePush')) { function queuePush(StateInterface $entity): void {