diff --git a/.github/workflows/image-retention.yaml b/.github/workflows/image-retention.yaml index 3ca941b9..6c725e4d 100644 --- a/.github/workflows/image-retention.yaml +++ b/.github/workflows/image-retention.yaml @@ -7,6 +7,9 @@ on: branches: - "*" - "!main" # excludes main + paths: + - "github-actions/cleanup-packages/**" + - ".github/workflows/image-retention.yaml" workflow_dispatch: jobs: diff --git a/php/examples/VaasExample/AuthenticationExamples.php b/php/examples/VaasExample/AuthenticationExamples.php index 93dc8a2c..cae61b94 100644 --- a/php/examples/VaasExample/AuthenticationExamples.php +++ b/php/examples/VaasExample/AuthenticationExamples.php @@ -2,7 +2,7 @@ namespace VaasExamples; -use VaasSdk\ClientCredentialsGrantAuthenticator; +use VaasSdk\Authentication\ClientCredentialsGrantAuthenticator; use VaasSdk\Exceptions\InvalidSha256Exception; use VaasSdk\Exceptions\TimeoutException; use VaasSdk\Exceptions\VaasAuthenticationException; diff --git a/php/examples/VaasExample/GetVerdictByFile.php b/php/examples/VaasExample/GetVerdictByFile.php index 7231fa18..df7de9cf 100644 --- a/php/examples/VaasExample/GetVerdictByFile.php +++ b/php/examples/VaasExample/GetVerdictByFile.php @@ -2,7 +2,7 @@ namespace VaasExamples; -use VaasSdk\ClientCredentialsGrantAuthenticator; +use VaasSdk\Authentication\ClientCredentialsGrantAuthenticator; use VaasSdk\Vaas; include_once("./vendor/autoload.php"); @@ -10,7 +10,7 @@ $authenticator = new ClientCredentialsGrantAuthenticator( getenv("CLIENT_ID"), getenv("CLIENT_SECRET"), - getenv("TOKEN_URL") ?? "https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token" + getenv("TOKEN_URL") ?: "https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token" ); $vaas = new Vaas( diff --git a/php/examples/VaasExample/GetVerdictByHash.php b/php/examples/VaasExample/GetVerdictByHash.php index cc56de68..287bd325 100644 --- a/php/examples/VaasExample/GetVerdictByHash.php +++ b/php/examples/VaasExample/GetVerdictByHash.php @@ -2,7 +2,7 @@ namespace VaasExamples; -use VaasSdk\ClientCredentialsGrantAuthenticator; +use VaasSdk\Authentication\ClientCredentialsGrantAuthenticator; use VaasSdk\Vaas; include_once("./vendor/autoload.php"); @@ -10,10 +10,10 @@ $authenticator = new ClientCredentialsGrantAuthenticator( getenv("CLIENT_ID"), getenv("CLIENT_SECRET"), - getenv("TOKEN_URL") ?? "https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token" + getenv("TOKEN_URL") ?: "https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token" ); $vaas = new Vaas( - getenv("VAAS_URL") ?? "wss://gateway.production.vaas.gdatasecurity.de" + getenv("VAAS_URL") ?: "wss://gateway.production.vaas.gdatasecurity.de" ); $vaas->Connect($authenticator->getToken()); diff --git a/php/examples/VaasExample/GetVerdictByUrl.php b/php/examples/VaasExample/GetVerdictByUrl.php index be835efe..529436cc 100644 --- a/php/examples/VaasExample/GetVerdictByUrl.php +++ b/php/examples/VaasExample/GetVerdictByUrl.php @@ -2,7 +2,7 @@ namespace VaasExamples; -use VaasSdk\ClientCredentialsGrantAuthenticator; +use VaasSdk\Authentication\ClientCredentialsGrantAuthenticator; use VaasSdk\Vaas; include_once("./vendor/autoload.php"); @@ -10,10 +10,10 @@ $authenticator = new ClientCredentialsGrantAuthenticator( getenv("CLIENT_ID"), getenv("CLIENT_SECRET"), - getenv("TOKEN_URL") ?? "https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token" + getenv("TOKEN_URL") ?: "https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token" ); $vaas = new Vaas( - getenv("VAAS_URL") ?? "wss://gateway.production.vaas.gdatasecurity.de" + getenv("VAAS_URL") ?: "wss://gateway.production.vaas.gdatasecurity.de" ); $vaas->Connect($authenticator->getToken()); diff --git a/php/phpunit.xml b/php/phpunit.xml index 121f5dee..cf5ecbef 100644 --- a/php/phpunit.xml +++ b/php/phpunit.xml @@ -1,6 +1,7 @@ + tests/vaas/StreamsInLoopTest.php tests/vaas/Sha256Test.php tests/vaas/VaasTest.php tests/vaas/ProtocolTest.php diff --git a/php/src/vaas/Authentication/ClientCredentialsGrantAuthenticator.php b/php/src/vaas/Authentication/ClientCredentialsGrantAuthenticator.php new file mode 100644 index 00000000..dde6b0b7 --- /dev/null +++ b/php/src/vaas/Authentication/ClientCredentialsGrantAuthenticator.php @@ -0,0 +1,20 @@ +_tokenReceiver = new OAuth2TokenReceiver($tokenEndpoint, $clientId, $clientSecret); + } + + public function getToken(): string + { + return $this->_tokenReceiver->GetToken(); + } +} diff --git a/php/src/vaas/Authentication/OAuth2TokenReceiver.php b/php/src/vaas/Authentication/OAuth2TokenReceiver.php new file mode 100644 index 00000000..de137670 --- /dev/null +++ b/php/src/vaas/Authentication/OAuth2TokenReceiver.php @@ -0,0 +1,69 @@ +_browser = HttpClientBuilder::buildDefault(); + $this->_tokenEndpoint = $tokenEndpoint; + $this->_clientId = $clientId; + $this->_clientSecret = $clientSecret; + $this->_username = $username; + $this->_password = $password; + $this->_grantType = $this->_clientSecret == "" ? "password" : "client_credentials"; + + $this->_formParams = new Form(); + $this->_formParams->addField('client_id', $this->_clientId); + $this->_formParams->addField('grant_type', $this->_grantType); + + switch($this->_grantType) { + case "password": + $this->_formParams->addField('username', $this->_username); + $this->_formParams->addField('password', $this->_password); + break; + case "client_credentials": + $this->_formParams->addField('client_secret', $this->_clientSecret); + break; + default: + throw new VaasAuthenticationException("Invalid grant type"); + } + } + + public function getToken() { + try { + $request = new Request($this->_tokenEndpoint, 'POST'); + $request->addHeader('Content-Type', 'application/x-www-form-urlencoded'); + $request->setBody($this->_formParams); + $response = $this->_browser->request($request, new TimeoutCancellation($this->_receiveTokenTimeout)); + if ($response->getStatus() != 200) { + throw new VaasAuthenticationException($response->getReason(), $response->getStatus()); + } + } catch (Exception $e) { + throw new VaasAuthenticationException($e->getMessage(), $e->getCode()); + } + $body = $response->getBody()->buffer(); + $response_body = json_decode($body); + return $response_body->access_token; + } +} \ No newline at end of file diff --git a/php/src/vaas/Authentication/ResourceOwnerPasswordGrantAuthenticator.php b/php/src/vaas/Authentication/ResourceOwnerPasswordGrantAuthenticator.php new file mode 100644 index 00000000..321e19c7 --- /dev/null +++ b/php/src/vaas/Authentication/ResourceOwnerPasswordGrantAuthenticator.php @@ -0,0 +1,21 @@ +_tokenReceiver = new OAuth2TokenReceiver($tokenEndpoint, $clientId, "", $userName, $password); + } + + /** + * @throws VaasAuthenticationException + */ + public function getToken() { + return $this->_tokenReceiver->GetToken(); + } +} diff --git a/php/src/vaas/ClientCredentialsGrantAuthenticator.php b/php/src/vaas/ClientCredentialsGrantAuthenticator.php deleted file mode 100644 index e28e6cd1..00000000 --- a/php/src/vaas/ClientCredentialsGrantAuthenticator.php +++ /dev/null @@ -1,53 +0,0 @@ -_clientId = $clientId; - $this->_clientSecret = $clientSecret; - $this->_tokenEndpoint = $tokenEndpoint; - $this->_httpClient = new HttpClient(); - } - - public function getToken(): string - { - $headers = ['Content-Type' => 'application/x-www-form-urlencoded']; - - try { - $response = $this->_httpClient->request( - 'POST', - $this->_tokenEndpoint, - [ - 'form_params' => [ - 'client_id' => $this->_clientId, - 'client_secret' => $this->_clientSecret, - 'grant_type' => "client_credentials" - ], - 'headers' => $headers - ] - ); - if ($response->getStatusCode() != 200) { - throw new VaasAuthenticationException($response->getReasonPhrase(), $response->getStatusCode()); - } - } catch (ClientException $e) { - throw new VaasAuthenticationException($e->getMessage(), $e->getCode()); - } - $response_body = json_decode($response->getBody()); - return $response_body->access_token; - } -} diff --git a/php/src/vaas/ResourceOwnerPasswordGrantAuthenticator.php b/php/src/vaas/ResourceOwnerPasswordGrantAuthenticator.php deleted file mode 100644 index 086d0730..00000000 --- a/php/src/vaas/ResourceOwnerPasswordGrantAuthenticator.php +++ /dev/null @@ -1,46 +0,0 @@ -clientId = $clientId; - $this->userName = $userName; - $this->password = $password; - $this->tokenEndpoint = $tokenEndpoint; - $this->verify = $verify; - } - - /** - * @throws VaasAuthenticationException - */ - public function getToken() { - $provider = new GenericProvider([ - 'clientId' => $this->clientId, - 'urlAuthorize' => $this->tokenEndpoint, - 'urlAccessToken' => $this->tokenEndpoint, - 'urlResourceOwnerDetails' => '', - 'verify' => $this->verify, - ]); - - try { - $accessToken = $provider->getAccessToken("password", [ - 'username' => $this->userName, - 'password' => $this->password - ]); - return $accessToken->getToken(); - } catch (IdentityProviderException $e) { - throw new VaasAuthenticationException($e->getMessage(), $e->getCode()); - } - } -} diff --git a/php/src/vaas/Vaas.php b/php/src/vaas/Vaas.php index 44dabb2d..0e1e70a4 100644 --- a/php/src/vaas/Vaas.php +++ b/php/src/vaas/Vaas.php @@ -2,9 +2,14 @@ namespace VaasSdk; -use GuzzleHttp\Client as HttpClient; -use GuzzleHttp\Exception\GuzzleException; -use GuzzleHttp\Psr7\Stream; +use Amp\ByteStream\ReadableResourceStream; +use Amp\ByteStream\ReadableStream; +use Amp\Http\Client\HttpClient; +use Amp\Http\Client\HttpClientBuilder; +use Amp\Http\Client\HttpException; +use Amp\Http\Client\Request; +use Amp\Http\Client\StreamedContent; +use Amp\TimeoutCancellation; use InvalidArgumentException; use JsonMapper; use JsonMapper_Exception; @@ -28,9 +33,12 @@ use VaasSdk\VaasOptions; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; +use Revolt\EventLoop; use VaasSdk\Message\BaseMessage; use VaasSdk\Message\VaasVerdict; use WebSocket\BadOpcodeException; +use WebSocket\Message\Close; +use WebSocket\Message\Ping; class Vaas { @@ -39,19 +47,16 @@ class Vaas private int $_waitTimeoutInSeconds = 600; private int $_uploadTimeoutInSeconds = 600; private LoggerInterface $_logger; - private HttpClient $_httpClient; private VaasOptions $_options; + private HttpClient $_httpClient; /** */ - public function __construct(?string $vaasUrl, ?LoggerInterface $logger = null, VaasOptions $options = new VaasOptions()) + public function __construct(?string $vaasUrl, ?LoggerInterface $logger = new NullLogger(), VaasOptions $options = new VaasOptions()) { $this->_options = $options; - $this->_httpClient = new HttpClient(); - if ($logger != null) - $this->_logger = $logger; - else - $this->_logger = new NullLogger(); + $this->_httpClient = HttpClientBuilder::buildDefault(); + $this->_logger = $logger; $this->_logger->debug("Url: " . $vaasUrl); if ($vaasUrl) $this->_vaasUrl = $vaasUrl; @@ -63,18 +68,13 @@ public function Connect( string $token, ?VaasConnection $vaasConnection = null ) { - if (isset($vaasConnection)) { - $this->_vaasConnection = $vaasConnection; - } else { - $this->_vaasConnection = new VaasConnection($this->_vaasUrl); - } + $this->_vaasConnection = $vaasConnection ?? new VaasConnection($this->_vaasUrl); $webSocket = $this->_vaasConnection->GetConnectedWebsocket(); $authRequest = new AuthRequest($token); $webSocket->send(json_encode($authRequest)); $authResponse = $this->_waitForAuthResponse(); - if ($this->_logger != null) - $this->_logger->debug("Authenticated: " . json_encode($authResponse)); + $this->_logger->debug("Authenticated: " . json_encode($authResponse)); $this->_vaasConnection->SessionId = $authResponse->session_id; } @@ -91,8 +91,7 @@ public function Connect( */ public function ForSha256(string $hashString, string $uuid = null): VaasVerdict { - if ($this->_logger != null) - $this->_logger->debug("ForSha256WithFlags", ["Sha256" => $hashString]); + $this->_logger->debug("ForSha256WithFlags", ["Sha256" => $hashString]); $sha256 = Sha256::TryFromString($hashString); @@ -117,7 +116,7 @@ public function ForSha256(string $hashString, string $uuid = null): VaasVerdict */ public function ForUrl(?string $url, string $uuid = null): VaasVerdict { - if ($this->_logger != null) $this->_logger->debug("ForUrlWithFlags", ["URL:" => $url]); + $this->_logger->debug("ForUrlWithFlags", ["URL:" => $url]); if (!filter_var($url, FILTER_VALIDATE_URL)) { throw new \InvalidArgumentException("Url is not valid"); @@ -145,29 +144,23 @@ public function ForUrl(?string $url, string $uuid = null): VaasVerdict */ public function ForFile(string $path, $upload = true, string $uuid = null): VaasVerdict { - if ($this->_logger != null) - $this->_logger->debug("ForFileWithFlags", ["File" => $path]); + $this->_logger->debug("ForFileWithFlags", ["File" => $path]); $sha256 = Sha256::TryFromFile($path); - if ($this->_logger != null) - $this->_logger->debug("Calculated Hash", ["Sha256" => $sha256]); + $this->_logger->debug("Calculated Hash", ["Sha256" => $sha256]); $verdictResponse = $this->_verdictResponseForSha256( $sha256, $uuid ); if ($verdictResponse->verdict == Verdict::UNKNOWN && $upload === true) { - if ($this->_logger != null) - $this->_logger->debug("UploadToken", ["UploadToken" => $verdictResponse->upload_token]); - $fileStream = fopen($path, 'r'); - $response = $this->_httpClient->put($verdictResponse->url, [ - 'body' => $fileStream, - 'timeout' => $this->_uploadTimeoutInSeconds, - 'headers' => ["Authorization" => $verdictResponse->upload_token] - ]); - if ($response->getStatusCode() > 399) { - throw new UploadFailedException($response->getReasonPhrase(), $response->getStatusCode()); - } + $this->_logger->debug("UploadToken", ["UploadToken" => $verdictResponse->upload_token]); + + $fileStream = new ReadableResourceStream(\fopen($path, 'r')); + $fileSize = \filesize($path); + + $this->UploadStream($fileStream, $verdictResponse->url, $verdictResponse->upload_token, $fileSize); + return new VaasVerdict($this->_waitForVerdict($verdictResponse->guid)); } @@ -177,7 +170,7 @@ public function ForFile(string $path, $upload = true, string $uuid = null): Vaas /** * Gets verdict by stream * - * @param string $path the path to get the verdict for + * @param ReadableStream $stream the path to get the verdict for * @param bool $upload should the file be uploaded if initial verdict is unknown * @param string $uuid unique identifier * @@ -187,18 +180,14 @@ public function ForFile(string $path, $upload = true, string $uuid = null): Vaas * @throws VaasServerException * @throws BadOpcodeException * @throws VaasInvalidStateException - * @throws GuzzleException * @throws UploadFailedException */ - public function ForStream(Stream $stream, string $uuid = null): VaasVerdict + public function ForStream(ReadableStream $stream, int $size = 0, string $uuid = null): VaasVerdict { - if ($uuid == null) { - $uuid = UuidV4::getFactory()->uuid4()->toString(); - } - - $verdictResponse = $this->_verdictResponseForStream( - $uuid - ); + $this->_logger->debug("uuid: ".var_export($uuid, true)); + $uuid = $uuid ?? UuidV4::getFactory()->uuid4()->toString(); + $this->_logger->debug("uuid: ".var_export($uuid, true)); + $verdictResponse = $this->_verdictResponseForStream($uuid); if ($verdictResponse->verdict != Verdict::UNKNOWN) { throw new VaasServerException("Server returned verdict without receiving content."); @@ -210,7 +199,7 @@ public function ForStream(Stream $stream, string $uuid = null): VaasVerdict throw new JsonMapper_Exception("VerdictResponse missing URL for stream upload."); } - $this->UploadStream($stream, $verdictResponse->url, $verdictResponse->upload_token); + $this->UploadStream($stream, $verdictResponse->url, $verdictResponse->upload_token, $size); $verdictResponse = $this->_waitForVerdict($uuid); @@ -230,8 +219,7 @@ public function ForStream(Stream $stream, string $uuid = null): VaasVerdict private function _waitForAuthResponse(): AuthResponse { $websocket = $this->_vaasConnection->GetConnectedWebsocket(); - if ($this->_logger != null) - $this->_logger->debug("WaitForAuthResponse"); + $this->_logger->debug("WaitForAuthResponse"); $start_time = time(); @@ -249,8 +237,15 @@ private function _waitForAuthResponse(): AuthResponse } if ($result != null) { - if ($this->_logger != null) - $this->_logger->debug("Result", json_decode($result, true)); + if ($result instanceof Ping) { + $websocket->pong(); + continue; + } + if ($result instanceof Close) { + throw new VaasServerException("Connection closed"); + } + $result = $result->getContent(); + $this->_logger->debug("Result", json_decode($result, true)); $genericObject = \json_decode($result); $resultObject = (new JsonMapper())->map( $genericObject, @@ -261,8 +256,7 @@ private function _waitForAuthResponse(): AuthResponse $genericObject, new AuthResponse() ); - if ($this->_logger != null) - $this->_logger->debug($result); + $this->_logger->debug($result); if ($authResponse->success === false) { throw new VaasAuthenticationException($result); } @@ -296,8 +290,7 @@ private function _waitForAuthResponse(): AuthResponse */ private function _waitForVerdict(string $guid): VerdictResponse { - if ($this->_logger != null) - $this->_logger->debug("WaitForVerdict"); + $this->_logger->debug("WaitForVerdict"); $start_time = time(); if (!isset($this->_vaasConnection)) { @@ -312,13 +305,19 @@ private function _waitForVerdict(string $guid): VerdictResponse try { $result = $websocket->receive(); } catch (\WebSocket\TimeoutException $e) { - if ($this->_logger != null) - $this->_logger->debug("Read timeout, send ping"); + $this->_logger->debug("Read timeout, send ping"); $websocket->ping(); } if ($result != null) { - if ($this->_logger != null) - $this->_logger->debug("Result", json_decode($result, true)); + if ($result instanceof Ping) { + $websocket->pong(); + continue; + } + if ($result instanceof Close) { + throw new VaasServerException("Connection closed"); + } + $result = $result->getContent(); + $this->_logger->debug("Result", json_decode($result, true)); $resultObject = json_decode($result); $baseMessage = (new JsonMapper())->map( $resultObject, @@ -352,7 +351,6 @@ private function _waitForVerdict(string $guid): VerdictResponse return $verdictResponse; } } - sleep(1); } } @@ -381,8 +379,7 @@ private function _handleWebSocketErrorResponse(Error $errorResponse): void */ private function _verdictResponseForSha256(Sha256 $sha256, string $uuid = null): VerdictResponse { - if ($this->_logger != null) - $this->_logger->debug("_verdictResponseForSha256"); + $this->_logger->debug("_verdictResponseForSha256"); if (!isset($this->_vaasConnection)) { throw new VaasInvalidStateException("connect() was not called"); @@ -394,8 +391,7 @@ private function _verdictResponseForSha256(Sha256 $sha256, string $uuid = null): $request->use_hash_lookup = $this->_options->UseHashLookup; $websocket->send(json_encode($request)); - if ($this->_logger != null) - $this->_logger->debug("verdictResponse", ["VerdictResponse" => json_encode($request)]); + $this->_logger->debug("verdictResponse", ["VerdictResponse" => json_encode($request)]); return $this->_waitForVerdict($request->guid); } @@ -407,8 +403,7 @@ private function _verdictResponseForSha256(Sha256 $sha256, string $uuid = null): */ private function _verdictResponseForUrl(string $url, string $uuid = null): VerdictResponse { - if ($this->_logger != null) - $this->_logger->debug("_verdictResponseForUrl"); + $this->_logger->debug("_verdictResponseForUrl"); if (!isset($this->_vaasConnection)) { throw new VaasInvalidStateException("connect() was not called"); @@ -420,8 +415,7 @@ private function _verdictResponseForUrl(string $url, string $uuid = null): Verdi $request->use_hash_lookup = $this->_options->UseHashLookup; $websocket->send(json_encode($request)); - if ($this->_logger != null) - $this->_logger->debug("verdictResponse", ["VerdictResponse" => json_encode($request)]); + $this->_logger->debug("verdictResponse", ["VerdictResponse" => json_encode($request)]); return $this->_waitForVerdict($request->guid); } @@ -436,8 +430,7 @@ private function _verdictResponseForUrl(string $url, string $uuid = null): Verdi */ private function _verdictResponseForStream(string $uuid = null): VerdictResponse { - if ($this->_logger != null) - $this->_logger->debug("_verdictResponseForStream"); + $this->_logger->debug("_verdictResponseForStream"); if (!isset($this->_vaasConnection)) { throw new VaasInvalidStateException("connect() was not called"); @@ -449,8 +442,7 @@ private function _verdictResponseForStream(string $uuid = null): VerdictResponse $request->use_hash_lookup = $this->_options->UseHashLookup; $websocket->send(json_encode($request)); - if ($this->_logger != null) - $this->_logger->debug("verdictResponse", ["VerdictResponse" => json_encode($request)]); + $this->_logger->debug("verdictResponse", ["VerdictResponse" => json_encode($request)]); return $this->_waitForVerdict($request->guid); } @@ -494,19 +486,44 @@ public function setUploadTimeout(int $UploadTimeoutInSeconds): self } /** - * @throws GuzzleException - * @throws UploadFailedException + * Uploads a file stream to a specified URL using a given upload token and file size. + * + * @param ReadableStream $fileStream The file stream to upload. + * @param string $url The URL to upload the file to. + * @param string $uploadToken The upload token to authenticate the upload. + * @param int $fileSize The size of the file being uploaded. + * @throws UploadFailedException If the upload fails. + * @throws VaasClientException If there is an error with the Vaas client. + * @return void */ - private function UploadStream(Stream $stream, string $url, string $uploadToken) + private function UploadStream(ReadableStream $fileStream, string $url, string $uploadToken, int $fileSize): void { - $response = $this->_httpClient->put($url, [ - 'body' => $stream, - 'content-length' => $stream->getSize(), - 'timeout' => $this->_uploadTimeoutInSeconds, - 'headers' => ["Authorization" => $uploadToken] - ]); - if ($response->getStatusCode() > 399) { - throw new UploadFailedException($response->getReasonPhrase(), $response->getStatusCode()); + $times = 0; + $pingTimer = EventLoop::repeat(5, function () use(&$times) { + $this->_logger->debug("pinging " . $times++); + $websocket = $this->_vaasConnection->GetAuthenticatedWebsocket(); + $websocket->ping(); + }); + + try { + $request = new Request($url, 'PUT'); + $request->setTransferTimeout($this->_uploadTimeoutInSeconds); + $request->setBody(StreamedContent::fromStream($fileStream, $fileSize)); + $request->addHeader("Content-Length", $fileSize); + $request->addHeader("Authorization", $uploadToken); + + $response = $this->_httpClient->request($request, new TimeoutCancellation($this->_uploadTimeoutInSeconds)); + if ($response->getStatus() > 399) { + $reason = $response->getBody()->buffer(); + throw new UploadFailedException($reason, $response->getStatus()); + } + } catch (\Exception $e) { + if ($e instanceof HttpException) { + throw new UploadFailedException($e->getMessage(), $e->getCode()); + } + throw new VaasClientException($e->getMessage()); + } finally { + EventLoop::cancel($pingTimer); } } } diff --git a/php/src/vaas/VaasConnection.php b/php/src/vaas/VaasConnection.php index a856431f..7e970c19 100644 --- a/php/src/vaas/VaasConnection.php +++ b/php/src/vaas/VaasConnection.php @@ -14,7 +14,12 @@ class VaasConnection public function __construct(string $url, Client $WebSocketClient = null) { if (!isset($WebSocketClient)) - $this->WebSocketClient = new Client($url); + $this->WebSocketClient = new Client($url, [ + "filter" => [ + 'text', 'binary', 'ping' + ], + "return_obj" => true + ]); else $this->WebSocketClient = $WebSocketClient; $this->WebSocketClient->ping(); diff --git a/php/src/vaas/composer.json b/php/src/vaas/composer.json index adfa0e43..ce994669 100644 --- a/php/src/vaas/composer.json +++ b/php/src/vaas/composer.json @@ -17,13 +17,12 @@ "ramsey/uuid": "^4.7 || ^4.2", "textalk/websocket": "^1.6 || ^1.5", "netresearch/jsonmapper": "^4.4", - "guzzlehttp/guzzle": "^7", "psr/log": "^1.1 || ^2.0 || ^3.0", - "league/oauth2-client": "^2.7" + "amphp/http-client": "^5.1" }, "autoload": { "psr-4": { "VaasSdk\\": "." } } -} \ No newline at end of file +} diff --git a/php/tests/vaas/ClientCredentialsGrantAuthenticatorTest.php b/php/tests/vaas/ClientCredentialsGrantAuthenticatorTest.php index 31b903eb..f45a43dc 100644 --- a/php/tests/vaas/ClientCredentialsGrantAuthenticatorTest.php +++ b/php/tests/vaas/ClientCredentialsGrantAuthenticatorTest.php @@ -2,13 +2,10 @@ namespace VaasTesting; -require_once __DIR__ . "/vendor/autoload.php"; - use Dotenv\Dotenv; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; -use Mockery; -use VaasSdk\ClientCredentialsGrantAuthenticator; +use VaasSdk\Authentication\ClientCredentialsGrantAuthenticator; use VaasSdk\Exceptions\VaasAuthenticationException; final class ClientCredentialsGrantAuthenticatorTest extends TestCase diff --git a/php/tests/vaas/ProtocolTest.php b/php/tests/vaas/ProtocolTest.php index 439dd664..93332ea8 100644 --- a/php/tests/vaas/ProtocolTest.php +++ b/php/tests/vaas/ProtocolTest.php @@ -2,8 +2,6 @@ namespace VaasTesting; -require_once __DIR__ . "/vendor/autoload.php"; - use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Psr\Log\NullLogger; @@ -27,7 +25,7 @@ public function testForConnectWithInvalidToken_ThrowsAccessDeniedException(): vo $fakeWebsocket->method('isConnected') ->willReturn(true); $fakeWebsocket->method('receive') - ->willReturn('{"kind": "AuthResponse", "success": false}'); + ->willReturn(new \WebSocket\Message\Text('{"kind": "AuthResponse", "success": false}')); $vaasConnection = new VaasConnection("", $fakeWebsocket); (new Vaas("url"))->Connect("invalid", $vaasConnection); @@ -43,7 +41,7 @@ public function testConnectionGetsClosedAfterConnecting_ThrowsVaasConnectionClos $fakeWebsocket->method('isConnected') ->willReturn(true, true, false); $fakeWebsocket->method('receive') - ->willReturn('{"kind": "AuthResponse", "success": true, "session_id": "id"}'); + ->willReturn(new \WebSocket\Message\Text('{"kind": "AuthResponse", "success": true, "session_id": "id"}')); $vaasConnection = new VaasConnection("", $fakeWebsocket); $vaas = new Vaas("url", new NullLogger()); diff --git a/php/tests/vaas/Sha256Test.php b/php/tests/vaas/Sha256Test.php index b41eed91..40b267e5 100644 --- a/php/tests/vaas/Sha256Test.php +++ b/php/tests/vaas/Sha256Test.php @@ -2,8 +2,6 @@ namespace VaasTesting; -require_once __DIR__ . "/vendor/autoload.php"; - use PHPUnit\Framework\TestCase; use VaasSdk\Sha256; use VaasSdk\Exceptions\InvalidSha256Exception; diff --git a/php/tests/vaas/StreamsInLoopTest.php b/php/tests/vaas/StreamsInLoopTest.php new file mode 100644 index 00000000..85830868 --- /dev/null +++ b/php/tests/vaas/StreamsInLoopTest.php @@ -0,0 +1,37 @@ +request(new ClientRequest("https://secure.eicar.org/eicar.com.txt", "GET")); + $bodyStream1 = $response1->getBody(); + $this->assertTrue($bodyStream1->isReadable()); + + $response2 = $browser2->request(new ClientRequest("https://secure.eicar.org/eicar.com", "GET")); + $bodyStream2 = $response2->getBody(); + $this->assertTrue($bodyStream1->isReadable()); + } +} \ No newline at end of file diff --git a/php/tests/vaas/VaasTest.php b/php/tests/vaas/VaasTest.php index da766c3e..4037a00b 100644 --- a/php/tests/vaas/VaasTest.php +++ b/php/tests/vaas/VaasTest.php @@ -2,43 +2,56 @@ namespace VaasTesting; -require_once __DIR__ . "/vendor/autoload.php"; - -use GuzzleHttp\Client; -use GuzzleHttp\Exception\GuzzleException; -use GuzzleHttp\Psr7\Stream; +use Amp\ByteStream\Pipe; +use Amp\ByteStream\WritableBuffer; +use Amp\Http\Client\HttpClient; +use Amp\Http\Client\HttpClientBuilder; +use Amp\Http\Client\Request; use JsonMapper_Exception; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; -use VaasSdk\ClientCredentialsGrantAuthenticator; +use VaasSdk\Authentication\ClientCredentialsGrantAuthenticator; +use VaasSdk\Authentication\ResourceOwnerPasswordGrantAuthenticator; use VaasSdk\Exceptions\TimeoutException; use VaasSdk\Exceptions\UploadFailedException; use VaasSdk\Exceptions\VaasAuthenticationException; use VaasSdk\Exceptions\VaasClientException; use VaasSdk\Exceptions\VaasServerException; -use VaasSdk\ResourceOwnerPasswordGrantAuthenticator; use VaasSdk\Vaas; use Dotenv\Dotenv; +use Exception; use Monolog\Formatter\JsonFormatter; use Monolog\Handler\StreamHandler; +use Monolog\Handler\TestHandler; +use Monolog\Level; use Psr\Log\LoggerInterface; use Monolog\Logger; +use Ramsey\Uuid\Generator\RandomBytesGenerator; use Ramsey\Uuid\Rfc4122\UuidV4; +use Revolt\EventLoop; use VaasSdk\Exceptions\VaasInvalidStateException; use VaasSdk\Message\Verdict; use VaasSdk\Sha256; use VaasSdk\VaasOptions; use WebSocket\BadOpcodeException; +use function Amp\ByteStream\Internal\tryToCreateReadableStreamFromResource; + final class VaasTest extends TestCase { use ProphecyTrait; const MALICIOUS_HASH = "ab5788279033b0a96f2d342e5f35159f103f69e0191dd391e036a1cd711791a2"; const MALICIOUS_URL = "https://secure.eicar.org/eicar.com.txt"; + private HttpClient $httpClient; public function setUp(): void { + $httpClientBuilder = (new HttpClientBuilder()) + ->skipAutomaticCompression() + ->skipDefaultAcceptHeader(); + $this->httpClient = $httpClientBuilder->build(); + $dotenv = Dotenv::createImmutable(__DIR__); $dotenv->safeLoad(); if (getenv("CLIENT_ID") !== false) { @@ -64,9 +77,9 @@ public function setUp(): void } } - private function _getVaas(bool $useCache = false, bool $useHashLookup = true): Vaas + private function _getVaas(bool $useCache = false, bool $useHashLookup = true, LoggerInterface $logger = null): Vaas { - return new Vaas($_ENV["VAAS_URL"], $this->_getDebugLogger(), new VaasOptions($useCache, $useHashLookup)); + return new Vaas($_ENV["VAAS_URL"], $logger ?? $this->_getDebugLogger(), new VaasOptions($useCache, $useHashLookup)); } private function _getDebugLogger(): LoggerInterface @@ -77,12 +90,12 @@ private function _getDebugLogger(): LoggerInterface if (in_array("--debug", $argv) === true) { $streamHandler = new StreamHandler( STDOUT, - Logger::DEBUG + Level::Debug ); } else { $streamHandler = new StreamHandler( STDOUT, - Logger::INFO + Level::Info ); } $streamHandler->setFormatter(new JsonFormatter()); @@ -458,11 +471,9 @@ public function testForStreamWithFlags_WithEicarString_ReturnsMalicious() $vaas = $this->_getVaas(); $vaas->Connect($this->getClientCredentialsGrantAuthenticator()->getToken()); $eicar = "X5O!P%@AP[4\\PZX54(P^)7CC)7}\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\$H+H*"; - $stream = fopen(sprintf('data://text/plain,%s', $eicar), 'r'); - rewind($stream); - $eicarStream = new Stream($stream); + $resourceStream = tryToCreateReadableStreamFromResource(fopen(sprintf('data://text/plain,%s', $eicar), 'r')); - $verdict = $vaas->ForStream($eicarStream); + $verdict = $vaas->ForStream($resourceStream, strlen($eicar)); $this->assertEquals(Verdict::MALICIOUS, $verdict->Verdict); } @@ -474,20 +485,18 @@ public function testForStreamWithFlags_WithEicarString_ReturnsMalicious() * @throws UploadFailedException * @throws TimeoutException * @throws BadOpcodeException - * @throws GuzzleException * @throws VaasAuthenticationException * @throws VaasInvalidStateException */ public function testForStream_WithEicarString_ReturnsMalicious() { - $vaas = $this->_getVaas(); + $vaas = $this->_getVaas(false, false); $vaas->Connect($this->getClientCredentialsGrantAuthenticator()->getToken()); - $eicar = "X5O!P%@AP[4\\PZX54(P^)7CC)7}\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\$H+H*"; - $stream = fopen(sprintf('data://text/plain,%s', $eicar), 'r'); - rewind($stream); - $eicarStream = new Stream($stream); + $eicar = 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'; + + $resourceStream = tryToCreateReadableStreamFromResource(fopen(sprintf('data://text/plain,%s', $eicar), 'r')); - $verdict = $vaas->ForStream($eicarStream); + $verdict = $vaas->ForStream($resourceStream, strlen($eicar)); $this->assertEquals(Verdict::MALICIOUS, $verdict->Verdict); } @@ -498,7 +507,6 @@ public function testForStream_WithEicarString_ReturnsMalicious() * @throws VaasServerException * @throws TimeoutException * @throws UploadFailedException - * @throws GuzzleException * @throws BadOpcodeException * @throws VaasInvalidStateException * @throws VaasAuthenticationException @@ -507,79 +515,159 @@ public function testForStream_WithCleanString_ReturnsClean() { $vaas = $this->_getVaas(); $vaas->Connect($this->getClientCredentialsGrantAuthenticator()->getToken()); - $clean = "I am a clean string"; - $stream = fopen(sprintf('data://text/plain,%s', $clean), 'r'); - rewind($stream); - $eicarStream = new Stream($stream); + $clean = str_replace("\0", "", (new RandomBytesGenerator())->generate(64)); + $resourceStream = tryToCreateReadableStreamFromResource(fopen(sprintf('data://text/plain,%s', $clean), 'r')); - $verdict = $vaas->ForStream($eicarStream); + $verdict = $vaas->ForStream($resourceStream, strlen($clean)); $this->assertEquals(Verdict::CLEAN, $verdict->Verdict); } /** - * @throws GuzzleException * @throws JsonMapper_Exception - * * @throws VaasClientException - * * @throws VaasServerException - * * @throws TimeoutException - * * @throws UploadFailedException - * * @throws GuzzleException - * * @throws BadOpcodeException - * * @throws VaasInvalidStateException - * * @throws VaasAuthenticationException + * @throws VaasClientException + * @throws VaasServerException + * @throws TimeoutException + * @throws UploadFailedException + * @throws BadOpcodeException + * @throws VaasInvalidStateException + * @throws VaasAuthenticationException */ public function testForStream_WithCleanUrlContentAsStream_ReturnsClean() { + $url = "https://raw.githubusercontent.com/GDATASoftwareAG/vaas/main/Readme.md"; + $request = new Request($url); + try { + $response = $this->httpClient->request($request); + if ($response->getStatus() != 200) { + throw new Exception($response->getReason(), $response->getStatus()); + } + } catch(Exception $e) { + echo $e->getMessage(); + } + $body = $response->getBody(); + $size = $response->getHeader("Content-Length"); + $vaas = $this->_getVaas(); $vaas->Connect($this->getClientCredentialsGrantAuthenticator()->getToken()); - $url = "https://raw.githubusercontent.com/GDATASoftwareAG/vaas/main/Readme.md"; - $httpClient = new Client(); - $response = $httpClient->get($url); - $stream = new Stream($response->getBody()->detach()); + $verdict = $vaas->ForStream($body, $size); + $this->assertEquals(Verdict::CLEAN, $verdict->Verdict); + } - $verdict = $vaas->ForStream($stream); + /** + * @throws JsonMapper_Exception + * @throws VaasClientException + * @throws VaasServerException + * @throws TimeoutException + * @throws UploadFailedException + * @throws BadOpcodeException + * @throws VaasInvalidStateException + * @throws VaasAuthenticationException + */ + public function testForStream_WithCleanDelayedfor11Seconds_ReturnsClean() + { + $monoLogger = new Logger("VaaS"); + $testHandler = new TestHandler(Level::Debug); + $monoLogger->pushHandler($testHandler); + $vaas = $this->_getVaas(false, false, $monoLogger); + $vaas->Connect($this->getClientCredentialsGrantAuthenticator()->getToken()); + + $randomString = $this->random_strings(11000); + + $stream = new Pipe(11000); + $writeTimer = EventLoop::repeat(0.001, function () use ($stream, &$randomString) { + if ($randomString === "") { + $stream->getSink()->end(); + return; + } + $data = substr($randomString, 0, 1); + $randomString = substr($randomString, 1); + $stream->getSink()->write($data); + }); + + try { + $verdict = $vaas->ForStream($stream->getSource(), 11000); + } catch(Exception $e) { + throw $e; + } finally { + EventLoop::cancel($writeTimer); + } + + $pingCount = 0; + foreach($testHandler->getRecords() as $record) { + if (stristr($record->message, "pinging") !== false) + $pingCount++; + }; + $this->assertEquals(2, $pingCount); $this->assertEquals(Verdict::CLEAN, $verdict->Verdict); } /** - * @throws GuzzleException * @throws JsonMapper_Exception - * * @throws VaasClientException - * * @throws VaasServerException - * * @throws TimeoutException - * * @throws UploadFailedException - * * @throws GuzzleException - * * @throws BadOpcodeException - * * @throws VaasInvalidStateException - * * @throws VaasAuthenticationException + * @throws VaasClientException + * @throws VaasServerException + * @throws TimeoutException + * @throws UploadFailedException + * @throws BadOpcodeException + * @throws VaasInvalidStateException + * @throws VaasAuthenticationException */ public function testForStream_WithEicarUrlContentAsStream_ReturnsMalicious() { + $request = new Request(self::MALICIOUS_URL); + try { + $response = $this->httpClient->request($request); + if ($response->getStatus() != 200) { + throw new Exception($response->getReason(), $response->getStatus()); + } + } catch(Exception $e) { + echo $e->getMessage(); + } + $body = $response->getBody(); + $size = $response->getHeader("Content-Length"); + $vaas = $this->_getVaas(); $vaas->Connect($this->getClientCredentialsGrantAuthenticator()->getToken()); - $httpClient = new Client(); - $response = $httpClient->get(self::MALICIOUS_URL); - $stream = new Stream($response->getBody()->detach()); - - $verdict = $vaas->ForStream($stream); + $verdict = $vaas->ForStream($body, $size); $this->assertEquals(Verdict::MALICIOUS, $verdict->Verdict); } public function testForStream_WithEicarUrlContentAsStream_ReturnsMaliciousWithDetectionAndMimeType() { + $request = new Request(self::MALICIOUS_URL); + try { + $response = $this->httpClient->request($request); + if ($response->getStatus() != 200) { + throw new Exception($response->getReason(), $response->getStatus()); + } + } catch(Exception $e) { + echo $e->getMessage(); + } + $body = $response->getBody(); + $size = $response->getHeader("Content-Length"); + $vaas = $this->_getVaas(); $vaas->Connect($this->getClientCredentialsGrantAuthenticator()->getToken()); - $httpClient = new Client(); - $response = $httpClient->get(self::MALICIOUS_URL); - $stream = new Stream($response->getBody()->detach()); - - $verdict = $vaas->ForStream($stream); + $verdict = $vaas->ForStream($body, $size); $this->assertEquals(Verdict::MALICIOUS, $verdict->Verdict); $this->assertEquals("text/plain", $verdict->MimeType); $this->assertNotEmpty($verdict->Detection); } + + static function random_strings($length_of_string) { + $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $characters_length = strlen($characters); + $random_string = ''; + + // Generate random characters until the string reaches desired length + for ($i = 0; $i < $length_of_string; $i++) { + $random_index = random_int(0, $characters_length - 1); + $random_string .= $characters[$random_index]; + } + + return $random_string; + } } diff --git a/php/tests/vaas/composer.json b/php/tests/vaas/composer.json index 9f93e7b4..4a30b125 100644 --- a/php/tests/vaas/composer.json +++ b/php/tests/vaas/composer.json @@ -14,10 +14,10 @@ "gdata/vaas": "999", "vlucas/phpdotenv": "^5.5", "monolog/monolog": "^3.3 || ^2.9", - "league/oauth2-client": "^2.4.0" + "amphp/http-client": "^5.1" }, "require-dev": { "phpunit/phpunit": "^11 || ^10", "phpspec/prophecy-phpunit": "^2" } -} \ No newline at end of file +} diff --git a/php/tests/vaas/phpunit.xml b/php/tests/vaas/phpunit.xml index a7605ad5..530e0897 100644 --- a/php/tests/vaas/phpunit.xml +++ b/php/tests/vaas/phpunit.xml @@ -1,6 +1,7 @@ - + + StreamsInLoopTest.php Sha256Test.php VaasTest.php ProtocolTest.php