agent/php/ElasticApm/Impl/AutoInstrument/CurlAutoInstrumentation.php (190 lines of code) (raw):

<?php /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** @noinspection PhpComposerExtensionStubsInspection */ declare(strict_types=1); namespace Elastic\Apm\Impl\AutoInstrument; use Elastic\Apm\Impl\AutoInstrument\Util\AutoInstrumentationUtil; use Elastic\Apm\Impl\Log\LogCategory; use Elastic\Apm\Impl\Log\Logger; use Elastic\Apm\Impl\Tracer; use Elastic\Apm\Impl\Util\DbgUtil; /** * Code in this file is part of implementation internals and thus it is not covered by the backward compatibility. * * @internal */ final class CurlAutoInstrumentation extends AutoInstrumentationBase { private const HANDLE_TRACKER_MAX_COUNT_HIGH_WATER_MARK = 2000; private const HANDLE_TRACKER_MAX_COUNT_LOW_WATER_MARK = 1000; private const CURL_INIT_ID = 1; public const CURL_SETOPT_ID = 2; public const CURL_SETOPT_ARRAY_ID = 3; public const CURL_COPY_HANDLE_ID = 4; public const CURL_EXEC_ID = 5; private const CURL_CLOSE_ID = 6; /** @var Logger */ private $logger; /** @var array<int, CurlHandleTracker> */ private $handleIdToTracker = []; public function __construct(Tracer $tracer) { parent::__construct($tracer); $this->logger = $tracer->loggerFactory()->loggerForClass(LogCategory::AUTO_INSTRUMENTATION, __NAMESPACE__, __CLASS__, __FILE__)->addContext('this', $this); } /** @inheritDoc */ public function name(): string { return InstrumentationNames::CURL; } /** @inheritDoc */ public function keywords(): array { return [InstrumentationKeywords::HTTP_CLIENT]; } /** @inheritDoc */ public function register(RegistrationContextInterface $ctx): void { if (!extension_loaded('curl')) { return; } $this->registerDelegatingToHandleTracker($ctx, 'curl_init', self::CURL_INIT_ID); $this->registerDelegatingToHandleTracker($ctx, 'curl_setopt', self::CURL_SETOPT_ID); $this->registerDelegatingToHandleTracker($ctx, 'curl_setopt_array', self::CURL_SETOPT_ARRAY_ID); $this->registerDelegatingToHandleTracker($ctx, 'curl_copy_handle', self::CURL_COPY_HANDLE_ID); $this->registerDelegatingToHandleTracker($ctx, 'curl_exec', self::CURL_EXEC_ID); $this->registerDelegatingToHandleTracker($ctx, 'curl_close', self::CURL_CLOSE_ID); } public function registerDelegatingToHandleTracker(RegistrationContextInterface $ctx, string $funcName, int $funcId): void { $ctx->interceptCallsToInternalFunction( $funcName, /** * @param mixed[] $interceptedCallArgs * * @return null|callable(int, bool, mixed): void */ function (array $interceptedCallArgs) use ($funcName, $funcId): ?callable { return $this->preHook($funcName, $funcId, $interceptedCallArgs); } ); } /** * @param string $funcName * @param int $funcId * @param mixed[] $interceptedCallArgs Intercepted call arguments * * @return null|callable(int, bool, mixed): void */ private function preHook(string $funcName, int $funcId, array $interceptedCallArgs): ?callable { $curlHandleTracker = $this->createHandleTracker($funcName, $funcId, $interceptedCallArgs); if ($curlHandleTracker === null) { return null; } /** * @param int $numberOfStackFramesToSkip * @param bool $hasExitedByException * @param mixed $returnValueOrThrown Return value of the intercepted call or thrown object * * @phpstan-param 0|positive-int $numberOfStackFramesToSkip */ return function (int $numberOfStackFramesToSkip, bool $hasExitedByException, $returnValueOrThrown) use ($funcName, $funcId, $interceptedCallArgs, $curlHandleTracker): void { /** @var 0|positive-int $numberOfStackFramesToSkip */ $this->postHook($funcName, $funcId, $interceptedCallArgs, $curlHandleTracker, $numberOfStackFramesToSkip + 1, $hasExitedByException, $returnValueOrThrown); }; } /** * @param string $dbgFuncName * @param int $funcId * @param mixed[] $interceptedCallArgs * @param CurlHandleTracker $curlHandleTracker * @param int $numberOfStackFramesToSkip * @param bool $hasExitedByException * @param mixed $returnValueOrThrown Return value of the intercepted call or thrown object * * @phpstan-param 0|positive-int $numberOfStackFramesToSkip */ private function postHook( string $dbgFuncName, int $funcId, array $interceptedCallArgs, CurlHandleTracker $curlHandleTracker, int $numberOfStackFramesToSkip, bool $hasExitedByException, $returnValueOrThrown ): void { AutoInstrumentationUtil::assertInterceptedCallNotExitedByException($hasExitedByException, ['functionName' => $dbgFuncName]); switch ($funcId) { case self::CURL_INIT_ID: case self::CURL_COPY_HANDLE_ID: $this->setTrackerHandle($curlHandleTracker, $returnValueOrThrown); return; // no need to handle self::CURL_CLOSE because null is returned in preHook default: $curlHandleTracker->postHook($dbgFuncName, $funcId, $numberOfStackFramesToSkip + 1, $interceptedCallArgs, $returnValueOrThrown); } } private function addToHandleIdToTracker(int $handleId, CurlHandleTracker $curlHandleTracker): void { ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Adding to curl handle ID to CurlHandleTracker map...', ['handleId' => $handleId]); $handleIdToTrackerCount = count($this->handleIdToTracker); if ($handleIdToTrackerCount >= self::HANDLE_TRACKER_MAX_COUNT_HIGH_WATER_MARK) { ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log( 'curl handle ID to CurlHandleTracker map reached its max capacity - purging it...', ['handleIdToTrackerCount' => $handleIdToTrackerCount] ); $this->handleIdToTracker = array_slice( $this->handleIdToTracker, $handleIdToTrackerCount - self::HANDLE_TRACKER_MAX_COUNT_LOW_WATER_MARK ); } $this->handleIdToTracker[$handleId] = $curlHandleTracker; } /** * @param Logger $logger * @param string $dbgFuncName * @param mixed[] $interceptedCallArgs * * @return ?CurlHandleWrapped */ public static function extractCurlHandleFromArgs(Logger $logger, string $dbgFuncName, array $interceptedCallArgs): ?CurlHandleWrapped { if (count($interceptedCallArgs) !== 0) { $curlHandle = $interceptedCallArgs[0]; if (CurlHandleWrapped::isValidValue($curlHandle)) { /** @var resource|object $curlHandle */ return new CurlHandleWrapped($curlHandle); } } $ctxToLog = [ 'functionName' => $dbgFuncName, 'count($interceptedCallArgs)' => count($interceptedCallArgs), ]; if (count($interceptedCallArgs) !== 0) { $ctxToLog['firstArgumentType'] = DbgUtil::getType($interceptedCallArgs[0]); $ctxToLog['interceptedCallArgs'] = $logger->possiblySecuritySensitive($interceptedCallArgs); } ($loggerProxy = $logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Expected curl handle to be the first argument but it is not', $ctxToLog); return null; } /** * @param string $dbgFuncName * @param mixed[] $interceptedCallArgs * * @return int|null */ private function findHandleId(string $dbgFuncName, array $interceptedCallArgs): ?int { $curlHandle = self::extractCurlHandleFromArgs($this->logger, $dbgFuncName, $interceptedCallArgs); if ($curlHandle === null) { return null; } $handleId = $curlHandle->asInt(); if (!array_key_exists($handleId, $this->handleIdToTracker)) { ($loggerProxy = $this->logger->ifWarningLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Not found in curl handle ID to CurlHandleTracker map', ['handleId' => $handleId]); return null; } return $handleId; } /** * @param string $dbgFuncName * @param mixed[] $interceptedCallArgs * * @return CurlHandleTracker|null */ private function findHandleTracker(string $dbgFuncName, array $interceptedCallArgs): ?CurlHandleTracker { $handleId = $this->findHandleId($dbgFuncName, $interceptedCallArgs); return $handleId === null ? null : $this->handleIdToTracker[$handleId]; } /** * @param string $dbgFuncName * @param int $funcId * @param mixed[] $interceptedCallArgs * * @return CurlHandleTracker|null */ public function createHandleTracker(string $dbgFuncName, int $funcId, array $interceptedCallArgs): ?CurlHandleTracker { switch ($funcId) { case self::CURL_INIT_ID: return $this->curlInitPreHook($interceptedCallArgs); case self::CURL_COPY_HANDLE_ID: return $this->curlCopyHandlePreHook($dbgFuncName, $interceptedCallArgs); case self::CURL_CLOSE_ID: $this->curlClosePreHook($dbgFuncName, $interceptedCallArgs); return null; default: $curlHandleTracker = $this->findHandleTracker($dbgFuncName, $interceptedCallArgs); if ($curlHandleTracker !== null) { $curlHandleTracker->preHook($dbgFuncName, $funcId, $interceptedCallArgs); } return $curlHandleTracker; } } /** * @param mixed[] $interceptedCallArgs * * @return CurlHandleTracker */ public function curlInitPreHook(array $interceptedCallArgs): CurlHandleTracker { $curlHandleTracker = new CurlHandleTracker($this->tracer); $curlHandleTracker->curlInitPreHook($interceptedCallArgs); return $curlHandleTracker; } /** * @param CurlHandleTracker $curlHandleTracker * @param mixed $curlHandle */ public function setTrackerHandle(CurlHandleTracker $curlHandleTracker, $curlHandle): void { $handleId = $curlHandleTracker->setHandle($curlHandle); if ($handleId !== null) { $this->addToHandleIdToTracker($handleId, $curlHandleTracker); } } /** * @param string $dbgFuncName * @param mixed[] $interceptedCallArgs * * @return CurlHandleTracker|null */ public function curlCopyHandlePreHook(string $dbgFuncName, array $interceptedCallArgs): ?CurlHandleTracker { $srcCurlHandleTracker = $this->findHandleTracker($dbgFuncName, $interceptedCallArgs); if ($srcCurlHandleTracker === null) { return null; } return $srcCurlHandleTracker->copy(); } /** * @param string $dbgFuncName * @param mixed[] $interceptedCallArgs */ public function curlClosePreHook(string $dbgFuncName, array $interceptedCallArgs): void { $handleId = $this->findHandleId($dbgFuncName, $interceptedCallArgs); if ($handleId === null) { return; } ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Removing from curl handle ID to CurlHandleTracker map...', ['handleId' => $handleId]); unset($this->handleIdToTracker[$handleId]); } }