Skip to content

Commit

Permalink
Tự động giải captcha (#2)
Browse files Browse the repository at this point in the history
* feat: Automatically sovle captcha

* feat(README): Add example

---------

Co-authored-by: ging-dev <[email protected]>
  • Loading branch information
thanhtran468 and ging-dev authored Jul 8, 2024
1 parent cbffdff commit 1db2cb8
Show file tree
Hide file tree
Showing 17 changed files with 212 additions and 22 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## IPay Unoffical Client

```php
<?php

declare(strict_types=1);

use IPay\IPayClient;

require __DIR__.'/vendor/autoload.php';

$ipay = IPayClient::create();

try {
$session = $ipay->guest()->login([
'userName' => 'yourUsername',
'accessCode' => 'yourPassword'
]);

$accountNumber = $session->customer()->accountNumber;

foreach ($session->historyTransactions([
'accountNumber' => $accountNumber,
'startDate' => new \DateTimeImmutable('-5 days'),
]) as $transaction) {
echo $transaction->remark.PHP_EOL;
}
} catch (Throwable $e) {
echo $e->getMessage();
}
```
9 changes: 6 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,19 @@
"php-http/client-common": "^2.7",
"nette/utils": "^4.0",
"eventsauce/object-hydrator": "^1.4",
"symfony/options-resolver": "^7.1"
"symfony/options-resolver": "^7.1",
"symfony/dom-crawler": "^7.1"
},
"require-dev": {
"php-http/curl-client": "^2.3",
"nyholm/psr7": "^1.8",
"phpstan/phpstan": "^1.11"
"phpstan/phpstan": "^1.11",
"pestphp/pest": "^2.34"
},
"config": {
"allow-plugins": {
"php-http/discovery": true
"php-http/discovery": true,
"pestphp/pest-plugin": true
}
}
}
17 changes: 17 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">./src</directory>
</include>
</source>
</phpunit>
8 changes: 4 additions & 4 deletions src/Api/AbstractApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,16 @@ public function __construct(
}

/**
* @param string[] $data
* @param string[] $parameters
*
* @return mixed[]
*/
protected function post(string $uri, array $data = []): array
protected function post(string $uri, array $parameters = []): array
{
$response = $this->iPayClient->getClient()->post(
$uri,
sprintf('ipay/wa/%s', $uri),
[],
BodyBuilder::from($data)
BodyBuilder::from($parameters)
->enhance($this->getSession()->getRequestParameters())
->build()
->encrypt()
Expand Down
6 changes: 3 additions & 3 deletions src/Api/AuthenticatedApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public function customer(): Customer
{
return $this->objectMapper->hydrateObject(
Customer::class,
$this->post('/getCustomerDetails')['customerInfo'],
$this->post('getCustomerDetails')['customerInfo'],
);
}

Expand All @@ -27,7 +27,7 @@ public function accounts(): array
{
return $this->objectMapper->hydrateObjects(
Account::class,
$this->post('/getEntitiesAndAccounts')['accounts'],
$this->post('getEntitiesAndAccounts')['accounts'],
)->toArray();
}

Expand Down Expand Up @@ -75,7 +75,7 @@ public function historyTransactions(array $parameters): \Iterator
$parameters['pageNumber'] = 0;
do {
$transactions = $this->post(
'/getHistTransactions',
'getHistTransactions',
$parameters
)['transactions'];
foreach ($this->objectMapper->hydrateObjects(
Expand Down
30 changes: 23 additions & 7 deletions src/Api/UnauthenticatedApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace IPay\Api;

use IPay\Captcha\CaptchaSolver;
use Nette\Utils\Random;

/**
* @extends AbstractApi<UnauthenticatedSession>
*/
Expand All @@ -11,8 +14,6 @@ class UnauthenticatedApi extends AbstractApi
* @param array{
* userName: string,
* accessCode: string,
* captchaCode: string,
* captchaId: string,
* } $credentials
*/
public function login(array $credentials): AuthenticatedApi
Expand All @@ -21,21 +22,36 @@ public function login(array $credentials): AuthenticatedApi
->setRequired([
'userName',
'accessCode',
'captchaCode',
'captchaId',
])
->setAllowedTypes('userName', 'string')
->setAllowedTypes('accessCode', 'string')
->setAllowedTypes('captchaCode', 'string')
->setAllowedTypes('captchaId', 'string')
;

[$captchaId, $captchaCode] = $this->bypassCaptcha();

$parameters = $resolver->resolve($credentials);
$parameters['captchaId'] = $captchaId;
$parameters['captchaCode'] = $captchaCode;

/** @var array{sessionId: string, ...} */
$result = $this->post('/signIn', $resolver->resolve($credentials));
$result = $this->post('signIn', $parameters);

return new AuthenticatedApi(
$this->iPayClient,
new AuthenticatedSession($result['sessionId'])
);
}

/**
* @return array{string,string}
*/
private function bypassCaptcha(): array
{
$captchaId = Random::generate(9, '0-9a-zA-Z');
$svg = (string) $this->iPayClient->getClient()
->get(sprintf('api/get-captcha/%s', $captchaId))
->getBody();

return [$captchaId, CaptchaSolver::solve($svg)];
}
}
44 changes: 44 additions & 0 deletions src/Captcha/CaptchaSolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace IPay\Captcha;

use Symfony\Component\DomCrawler\Crawler;

final class CaptchaSolver
{
private const LENGTH_TO_NUMBER_MAPPING = [
66 => 0,
49 => 1,
53 => 2,
70 => 3,
76 => 4,
58 => 5,
64 => 6,
50 => 7,
75 => 8,
69 => 9,
];

public static function solve(string $svg): string
{
/** @var string[] */
$svgPathCommands = (new Crawler($svg))
->filterXPath('//path[@fill!="none"]')
->each(function (Crawler $node): string {
return (string) $node->attr('d');
});

sort($svgPathCommands, SORT_NATURAL);

$result = '';
foreach ($svgPathCommands as $svgPathCommand) {
/** @var string */
$symbolicPath = preg_replace('/[^A-Z]/', '', $svgPathCommand);
$result .= static::LENGTH_TO_NUMBER_MAPPING[
strlen($symbolicPath)
];
}

return $result;
}
}
12 changes: 8 additions & 4 deletions src/Http/Plugin/ExceptionThrower.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Http\Client\Common\Plugin;
use Http\Promise\Promise;
use IPay\Exception\LoginFailedException;
use IPay\Exception\SessionExpiredException;
use Nette\Utils\Json;
use Psr\Http\Message\RequestInterface;
Expand All @@ -15,19 +16,22 @@ public function handleRequest(RequestInterface $request, callable $next, callabl
{
return $next($request)->then(function (ResponseInterface $response): ResponseInterface {
if (200 !== $response->getStatusCode()) {
/** @var object{errorCode:string} */
$error = Json::decode((string) $response->getBody());

throw self::createException($error->errorCode);
throw self::createException($error);
}

return $response;
});
}

private static function createException(string $code): \Throwable
/**
* @param \stdClass&object{errorCode: string, errorMessage: string} $error
*/
private static function createException(\stdClass $error): \Throwable
{
return match ($code) {
return match ($error->errorCode) {
'LOGON_CREDENTIALS_REJECTED' => new LoginFailedException($error->errorMessage),
'96', '99' => new SessionExpiredException('The session has expired.'),
default => new \RuntimeException('Unknown error.'),
};
Expand Down
2 changes: 1 addition & 1 deletion src/IPayClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public static function create(): static
new HttpMethodsClient(
new PluginClient(Psr18ClientDiscovery::find(), [
new BaseUriPlugin(Psr17FactoryDiscovery::findUriFactory()
->createUri('https://api-ipay.vietinbank.vn/ipay/wa')
->createUri('https://api-ipay.vietinbank.vn')
),
new ContentTypePlugin(),
new ExceptionThrower(),
Expand Down
11 changes: 11 additions & 0 deletions tests/Feature/CaptchaTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php
use IPay\Captcha\CaptchaSolver;

test('captcha solver', function (string $filename, string $result) {
$svg = file_get_contents($filename);
expect(CaptchaSolver::solve($svg))->toBe($result);
})->with([
'captcha 1' => [__DIR__.'/Fixture/1.svg', '540701'],
'captcha 2' => [__DIR__.'/Fixture/2.svg', '826778'],
'captcha 3' => [__DIR__.'/Fixture/3.svg', '835697'],
]);
3 changes: 3 additions & 0 deletions tests/Feature/ExampleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

// nothing
1 change: 1 addition & 0 deletions tests/Feature/Fixture/1.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 1db2cb8

Please sign in to comment.