├── .gitignore
├── .travis.yml
├── README.md
├── _config.yml
├── codeception.yml
├── composer.json
├── composer.lock
├── src
└── Esia
│ ├── Config.php
│ ├── Exceptions
│ ├── AbstractEsiaException.php
│ ├── ForbiddenException.php
│ ├── InvalidConfigurationException.php
│ └── RequestFailException.php
│ ├── Http
│ ├── Exceptions
│ │ └── HttpException.php
│ └── GuzzleHttpClient.php
│ ├── OpenId.php
│ └── Signer
│ ├── AbstractSignerPKCS7.php
│ ├── CliSignerPKCS7.php
│ ├── Exceptions
│ ├── CannotGenerateRandomIntException.php
│ ├── CannotReadCertificateException.php
│ ├── CannotReadPrivateKeyException.php
│ ├── NoSuchCertificateFileException.php
│ ├── NoSuchKeyFileException.php
│ ├── NoSuchTmpDirException.php
│ └── SignFailException.php
│ ├── SignerInterface.php
│ └── SignerPKCS7.php
└── tests
├── .configure-gost-openssl.sh
├── _bootstrap.php
├── _data
├── non_readable_file
├── server-gost.crt
├── server-gost.key
├── server.crt
├── server.csr
└── server.key
├── _support
├── Helper
│ └── Unit.php
├── UnitTester.php
└── _generated
│ └── UnitTesterActions.php
├── unit.suite.yml
└── unit
├── ConfigTest.php
├── Http
└── GuzzleHttpClientTest.php
├── OpenIdCliOpensslTest.php
├── OpenIdTest.php
├── Signer
└── SignerPKCS7Test.php
└── _bootstrap.php
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | tests/tmp/*
3 | tests/_output/*
4 | public/*
5 | vendor
6 |
7 | tests/_data/non_readable_file
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: bionic
2 | language: php
3 | addons:
4 | apt:
5 | packages:
6 | - libengine-gost-openssl1.1
7 |
8 | before_install:
9 | - sudo bash tests/.configure-gost-openssl.sh
10 |
11 | php:
12 | - 7.1
13 | - 7.2
14 | - 7.3
15 | - 7.4
16 | - 8.0
17 |
18 | install:
19 | - travis_retry composer self-update
20 | - travis_retry composer --version
21 | - travis_retry composer update --prefer-dist --no-interaction
22 |
23 | script:
24 | - chmod 000 tests/_data/non_readable_file
25 | - php vendor/codeception/codeception/codecept run
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Единая система идентификации и аутентификации (ЕСИА) OpenId
3 |
4 | [](https://travis-ci.org/fr05t1k/esia)
5 |
6 | # Описание
7 | Компонент для авторизации на портале "Госуслуги".
8 |
9 | # Внимание!
10 | Получив токен вы можете выполнять любые API запросы. Библиотека не поддерживает все существующие методы в API, а предоставляет только самые базовые. Основная цель библиотеки - получение токена.
11 |
12 | # Установка
13 |
14 | При помощи [composer](https://getcomposer.org/download/):
15 | ```
16 | composer require --prefer-dist fr05t1k/esia
17 | ```
18 | Или добавьте в composer.json
19 |
20 | ```
21 | "fr05t1k/esia" : "^2.0"
22 | ```
23 |
24 | # Как использовать
25 |
26 | Пример получения ссылки для авторизации
27 | ```php
28 | 'INSP03211',
31 | 'redirectUrl' => 'http://my-site.com/response.php',
32 | 'portalUrl' => 'https://esia-portal1.test.gosuslugi.ru/',
33 | 'scope' => ['fullname', 'birthdate'],
34 | ]);
35 | $esia = new \Esia\OpenId($config);
36 | $esia->setSigner(new \Esia\Signer\SignerPKCS7(
37 | 'my-site.com.pem',
38 | 'my-site.com.pem',
39 | 'password',
40 | '/tmp'
41 | ));
42 | ?>
43 |
44 | Войти через портал госуслуги
45 | ```
46 |
47 | После редиректа на ваш `redirectUrl` вы получите в `$_GET['code']` код для получения токена
48 |
49 | Пример получения токена и информации о пользователе
50 |
51 | ```php
52 |
53 | $esia = new \Esia\OpenId($config);
54 |
55 | // Вы можете использовать токен в дальнейшем вместе с oid
56 | $token = $esia->getToken($_GET['code']);
57 |
58 | $personInfo = $esia->getPersonInfo();
59 | $addressInfo = $esia->getAddressInfo();
60 | $contactInfo = $esia->getContactInfo();
61 | $documentInfo = $esia->getDocInfo();
62 |
63 | ```
64 | # Конфиг
65 |
66 | `clientId` - ID вашего приложения.
67 |
68 | `redirectUrl` - URL куда будет перенаправлен ответ с кодом.
69 |
70 | `portalUrl` - по умолчанию: `https://esia-portal1.test.gosuslugi.ru/`. Домен портала для авторизация (только домен).
71 |
72 | `codeUrlPath` - по умолчанию: `aas/oauth2/ac`. URL для получения кода.
73 |
74 | `tokenUrlPath` - по умолчанию: `aas/oauth2/te`. URL для получение токена.
75 |
76 | `scope` - по умолчанию: `fullname birthdate gender email mobile id_doc snils inn`. Запрашиваемые права у пользователя.
77 |
78 | `privateKeyPath` - путь до приватного ключа.
79 |
80 | `privateKeyPassword` - пароль от приватного ключа.
81 |
82 | `certPath` - путь до сертификата.
83 |
84 | `tmpPath` - путь до дериктории где будет проходить подпись (должна быть доступна для записи).
85 |
86 | # Токен и oid
87 |
88 | Токен - jwt токен которые вы получаете от ЕСИА для дальнейшего взаимодействия
89 |
90 | oid - уникальный идентификатор владельца токена
91 |
92 | ## Как получить oid?
93 | Если 2 способа:
94 | 1. oid содержится в jwt токене, расшифровав его
95 | 2. После получения токена oid сохраняется в config и получить можно так
96 | ```php
97 | $esia->getConfig()->getOid();
98 | ```
99 |
100 | ## Переиспользование Токена
101 |
102 | Дополнительно укажите токен и идентификатор в конфиге
103 | ```php
104 | $config->setToken($jwt);
105 | $config->setOid($oid);
106 | ```
107 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
--------------------------------------------------------------------------------
/codeception.yml:
--------------------------------------------------------------------------------
1 | actor: Tester
2 | paths:
3 | tests: tests
4 | log: tests/_output
5 | data: tests/_data
6 | support: tests/_support
7 | envs: tests/_envs
8 | bootstrap: _bootstrap.php
9 | settings:
10 | colors: true
11 | memory_limit: 1024M
12 | extensions:
13 | enabled:
14 | - Codeception\Extension\RunFailed
15 | coverage:
16 | enabled: true
17 | remote: false
18 | whitelist:
19 | include:
20 | - src/*
21 |
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fr05t1k/esia",
3 | "license": "MIT",
4 | "description": "OpenID ESIA authenticating",
5 | "keywords": [
6 | "esia",
7 | "openid",
8 | "egov"
9 | ],
10 | "autoload": {
11 | "psr-4": {
12 | "Esia\\": "src/Esia"
13 | }
14 | },
15 | "autoload-dev": {
16 | "psr-4": {
17 | "tests\\" : "tests"
18 | }
19 | },
20 | "require": {
21 | "php": "^7.1|^8.0",
22 | "guzzlehttp/guzzle": "^6.1.0|^7.0",
23 | "psr/log": "^1.0",
24 | "psr/http-message": "^1.0",
25 | "psr/http-client": "^1.0"
26 | },
27 | "suggest": {
28 | "ext-openssl": "SignerPKCS7 support"
29 | },
30 | "require-dev": {
31 | "roave/security-advisories": "dev-latest",
32 | "codeception/codeception": "^4.0",
33 | "codeception/module-asserts": "^1.3"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Esia/Config.php:
--------------------------------------------------------------------------------
1 | clientId = $config['clientId'] ?? $this->clientId;
49 | if (!$this->clientId) {
50 | throw new InvalidConfigurationException('Please provide clientId');
51 | }
52 |
53 | $this->redirectUrl = $config['redirectUrl'] ?? $this->redirectUrl;
54 | if (!$this->redirectUrl) {
55 | throw new InvalidConfigurationException('Please provide redirectUrl');
56 | }
57 |
58 | $this->privateKeyPath = $config['privateKeyPath'] ?? $this->privateKeyPath;
59 | if (!$this->privateKeyPath) {
60 | throw new InvalidConfigurationException('Please provide privateKeyPath');
61 | }
62 | $this->certPath = $config['certPath'] ?? $this->certPath;
63 | if (!$this->certPath) {
64 | throw new InvalidConfigurationException('Please provide certPath');
65 | }
66 |
67 | $this->portalUrl = $config['portalUrl'] ?? $this->portalUrl;
68 | $this->tokenUrlPath = $config['tokenUrlPath'] ?? $this->tokenUrlPath;
69 | $this->codeUrlPath = $config['codeUrlPath'] ?? $this->codeUrlPath;
70 | $this->personUrlPath = $config['personUrlPath'] ?? $this->personUrlPath;
71 | $this->logoutUrlPath = $config['logoutUrlPath'] ?? $this->logoutUrlPath;
72 | $this->privateKeyPassword = $config['privateKeyPassword'] ?? $this->privateKeyPassword;
73 | $this->oid = $config['oid'] ?? $this->oid;
74 | $this->scope = $config['scope'] ?? $this->scope;
75 | if (!is_array($this->scope)) {
76 | throw new InvalidConfigurationException('scope must be array of strings');
77 | }
78 |
79 | $this->responseType = $config['responseType'] ?? $this->responseType;
80 | $this->accessType = $config['accessType'] ?? $this->accessType;
81 | $this->tmpPath = $config['tmpPath'] ?? $this->tmpPath;
82 | $this->token = $config['token'] ?? $this->token;
83 | }
84 |
85 | public function getPortalUrl(): string
86 | {
87 | return $this->portalUrl;
88 | }
89 |
90 | public function getPrivateKeyPath(): string
91 | {
92 | return $this->privateKeyPath;
93 | }
94 |
95 | public function getPrivateKeyPassword(): string
96 | {
97 | return $this->privateKeyPassword;
98 | }
99 |
100 | public function getCertPath(): string
101 | {
102 | return $this->certPath;
103 | }
104 |
105 | public function getOid(): string
106 | {
107 | return $this->oid;
108 | }
109 |
110 | public function setOid(string $oid): void
111 | {
112 | $this->oid = $oid;
113 | }
114 |
115 | public function getScope(): array
116 | {
117 | return $this->scope;
118 | }
119 |
120 | public function getScopeString(): string
121 | {
122 | return implode(' ', $this->scope);
123 | }
124 |
125 | public function getResponseType(): string
126 | {
127 | return $this->responseType;
128 | }
129 |
130 | public function getAccessType(): string
131 | {
132 | return $this->accessType;
133 | }
134 |
135 | public function getTmpPath(): string
136 | {
137 | return $this->tmpPath;
138 | }
139 |
140 | public function getToken(): ?string
141 | {
142 | return $this->token;
143 | }
144 |
145 | public function setToken(string $token): void
146 | {
147 | $this->token = $token;
148 | }
149 |
150 | public function getClientId(): string
151 | {
152 | return $this->clientId;
153 | }
154 |
155 | public function getRedirectUrl(): string
156 | {
157 | return $this->redirectUrl;
158 | }
159 |
160 | /**
161 | * Return an url for request to get an access token
162 | */
163 | public function getTokenUrl(): string
164 | {
165 | return $this->portalUrl . $this->tokenUrlPath;
166 | }
167 |
168 | /**
169 | * Return an url for request to get an authorization code
170 | */
171 | public function getCodeUrl(): string
172 | {
173 | return $this->portalUrl . $this->codeUrlPath;
174 | }
175 |
176 | /**
177 | * @return string
178 | * @throws InvalidConfigurationException
179 | */
180 | public function getPersonUrl(): string
181 | {
182 | if (!$this->oid) {
183 | throw new InvalidConfigurationException('Please provide oid');
184 | }
185 | return $this->portalUrl . $this->personUrlPath . '/' . $this->oid;
186 | }
187 |
188 | /**
189 | * Return an url for logout
190 | */
191 | public function getLogoutUrl(): string
192 | {
193 | return $this->portalUrl . $this->logoutUrlPath;
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/src/Esia/Exceptions/AbstractEsiaException.php:
--------------------------------------------------------------------------------
1 | guzzle = $guzzle;
26 | }
27 |
28 | /**
29 | * Sends a PSR-7 request and returns a PSR-7 response.
30 | *
31 | * Every technically correct HTTP response MUST be returned as is, even if it represents a HTTP
32 | * error response or a redirect instruction. The only special case is 1xx responses, which MUST
33 | * be assembled in the HTTP client.
34 | *
35 | * The client MAY do modifications to the Request before sending it. Because PSR-7 objects are
36 | * immutable, one cannot assume that the object passed to ClientInterface::sendRequest() will be the same
37 | * object that is actually sent. For example the Request object that is returned by an exception MAY
38 | * be a different object than the one passed to sendRequest, so comparison by reference (===) is not possible.
39 | *
40 | * {@link https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message-meta.md#why-value-objects}
41 | *
42 | * @param RequestInterface $request
43 | *
44 | * @return ResponseInterface
45 | *
46 | * @throws ClientExceptionInterface If an error happens during processing the request.
47 | */
48 | public function sendRequest(RequestInterface $request): ResponseInterface
49 | {
50 | try {
51 | return $this->guzzle->send($request);
52 | } catch (GuzzleException $e) {
53 | throw new HttpException($e->getMessage(), $e->getCode(), $e);
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Esia/OpenId.php:
--------------------------------------------------------------------------------
1 | config = $config;
54 | $this->client = $client ?? new GuzzleHttpClient(new Client());
55 | $this->logger = new NullLogger();
56 | $this->signer = new SignerPKCS7(
57 | $config->getCertPath(),
58 | $config->getPrivateKeyPath(),
59 | $config->getPrivateKeyPassword(),
60 | $config->getTmpPath()
61 | );
62 | }
63 |
64 | /**
65 | * Replace default signer
66 | */
67 | public function setSigner(SignerInterface $signer): void
68 | {
69 | $this->signer = $signer;
70 | }
71 |
72 | /**
73 | * Get config
74 | */
75 | public function getConfig(): Config
76 | {
77 | return $this->config;
78 | }
79 |
80 | /**
81 | * Return an url for authentication
82 | *
83 | * ```php
84 | * Login
85 | * ```
86 | *
87 | * @return string|false
88 | * @throws SignFailException
89 | */
90 | public function buildUrl()
91 | {
92 | $timestamp = $this->getTimeStamp();
93 | $state = $this->buildState();
94 | $message = $this->config->getScopeString()
95 | . $timestamp
96 | . $this->config->getClientId()
97 | . $state;
98 |
99 | $clientSecret = $this->signer->sign($message);
100 |
101 | $url = $this->config->getCodeUrl() . '?%s';
102 |
103 | $params = [
104 | 'client_id' => $this->config->getClientId(),
105 | 'client_secret' => $clientSecret,
106 | 'redirect_uri' => $this->config->getRedirectUrl(),
107 | 'scope' => $this->config->getScopeString(),
108 | 'response_type' => $this->config->getResponseType(),
109 | 'state' => $state,
110 | 'access_type' => $this->config->getAccessType(),
111 | 'timestamp' => $timestamp,
112 | ];
113 |
114 | $request = http_build_query($params);
115 |
116 | return sprintf($url, $request);
117 | }
118 |
119 | /**
120 | * Return an url for logout
121 | */
122 | public function buildLogoutUrl(string $redirectUrl = null): string
123 | {
124 | $url = $this->config->getLogoutUrl() . '?%s';
125 | $params = [
126 | 'client_id' => $this->config->getClientId(),
127 | ];
128 |
129 | if ($redirectUrl) {
130 | $params['redirect_url'] = $redirectUrl;
131 | }
132 |
133 | $request = http_build_query($params);
134 |
135 | return sprintf($url, $request);
136 | }
137 |
138 | /**
139 | * Method collect a token with given code
140 | *
141 | * @throws SignFailException
142 | * @throws AbstractEsiaException
143 | */
144 | public function getToken(string $code): string
145 | {
146 | $timestamp = $this->getTimeStamp();
147 | $state = $this->buildState();
148 |
149 | $clientSecret = $this->signer->sign(
150 | $this->config->getScopeString()
151 | . $timestamp
152 | . $this->config->getClientId()
153 | . $state
154 | );
155 |
156 | $body = [
157 | 'client_id' => $this->config->getClientId(),
158 | 'code' => $code,
159 | 'grant_type' => 'authorization_code',
160 | 'client_secret' => $clientSecret,
161 | 'state' => $state,
162 | 'redirect_uri' => $this->config->getRedirectUrl(),
163 | 'scope' => $this->config->getScopeString(),
164 | 'timestamp' => $timestamp,
165 | 'token_type' => 'Bearer',
166 | 'refresh_token' => $state,
167 | ];
168 |
169 | $payload = $this->sendRequest(
170 | new Request(
171 | 'POST',
172 | $this->config->getTokenUrl(),
173 | [
174 | 'Content-Type' => 'application/x-www-form-urlencoded',
175 | ],
176 | http_build_query($body)
177 | )
178 | );
179 |
180 | $this->logger->debug('Payload: ', $payload);
181 |
182 | $token = $payload['access_token'];
183 | $this->config->setToken($token);
184 |
185 | # get object id from token
186 | $chunks = explode('.', $token);
187 | $payload = json_decode($this->base64UrlSafeDecode($chunks[1]), true);
188 | $this->config->setOid($payload['urn:esia:sbj_id']);
189 |
190 | return $token;
191 | }
192 |
193 | /**
194 | * Fetch person info from current person
195 | *
196 | * You must collect token person before
197 | * calling this method
198 | *
199 | * @throws AbstractEsiaException
200 | */
201 | public function getPersonInfo(): array
202 | {
203 | $url = $this->config->getPersonUrl();
204 |
205 | return $this->sendRequest(new Request('GET', $url));
206 | }
207 |
208 | /**
209 | * Fetch contact info about current person
210 | *
211 | * You must collect token person before
212 | * calling this method
213 | *
214 | * @throws Exceptions\InvalidConfigurationException
215 | * @throws AbstractEsiaException
216 | */
217 | public function getContactInfo(): array
218 | {
219 | $url = $this->config->getPersonUrl() . '/ctts';
220 | $payload = $this->sendRequest(new Request('GET', $url));
221 |
222 | if ($payload && $payload['size'] > 0) {
223 | return $this->collectArrayElements($payload['elements']);
224 | }
225 |
226 | return $payload;
227 | }
228 |
229 |
230 | /**
231 | * Fetch address from current person
232 | *
233 | * You must collect token person before
234 | * calling this method
235 | *
236 | * @throws Exceptions\InvalidConfigurationException
237 | * @throws AbstractEsiaException
238 | */
239 | public function getAddressInfo(): array
240 | {
241 | $url = $this->config->getPersonUrl() . '/addrs';
242 | $payload = $this->sendRequest(new Request('GET', $url));
243 |
244 | if ($payload['size'] > 0) {
245 | return $this->collectArrayElements($payload['elements']);
246 | }
247 |
248 | return $payload;
249 | }
250 |
251 | /**
252 | * Fetch documents info about current person
253 | *
254 | * You must collect token person before
255 | * calling this method
256 | *
257 | * @throws Exceptions\InvalidConfigurationException
258 | * @throws AbstractEsiaException
259 | */
260 | public function getDocInfo(): array
261 | {
262 | $url = $this->config->getPersonUrl() . '/docs';
263 |
264 | $payload = $this->sendRequest(new Request('GET', $url));
265 |
266 | if ($payload && $payload['size'] > 0) {
267 | return $this->collectArrayElements($payload['elements']);
268 | }
269 |
270 | return $payload;
271 | }
272 |
273 | /**
274 | * This method can iterate on each element
275 | * and fetch entities from esia by url
276 | *
277 | * @throws AbstractEsiaException
278 | */
279 | private function collectArrayElements($elements): array
280 | {
281 | $result = [];
282 | foreach ($elements as $elementUrl) {
283 | $elementPayload = $this->sendRequest(new Request('GET', $elementUrl));
284 |
285 | if ($elementPayload) {
286 | $result[] = $elementPayload;
287 | }
288 | }
289 |
290 | return $result;
291 | }
292 |
293 | /**
294 | * @throws AbstractEsiaException
295 | */
296 | private function sendRequest(RequestInterface $request): array
297 | {
298 | try {
299 | if ($this->config->getToken()) {
300 | /** @noinspection CallableParameterUseCaseInTypeContextInspection */
301 | $request = $request->withHeader('Authorization', 'Bearer ' . $this->config->getToken());
302 | }
303 | $response = $this->client->sendRequest($request);
304 | $responseBody = json_decode($response->getBody()->getContents(), true);
305 |
306 | if (!is_array($responseBody)) {
307 | throw new RuntimeException(
308 | sprintf(
309 | 'Cannot decode response body. JSON error (%d): %s',
310 | json_last_error(),
311 | json_last_error_msg()
312 | )
313 | );
314 | }
315 |
316 | return $responseBody;
317 | } catch (ClientExceptionInterface $e) {
318 | $this->logger->error('Request was failed', ['exception' => $e]);
319 | $prev = $e->getPrevious();
320 |
321 | // Only for Guzzle
322 | if ($prev instanceof BadResponseException
323 | && $prev->getResponse() !== null
324 | && $prev->getResponse()->getStatusCode() === 403
325 | ) {
326 | throw new ForbiddenException('Request is forbidden', 0, $e);
327 | }
328 |
329 | throw new RequestFailException('Request is failed', 0, $e);
330 | } catch (RuntimeException $e) {
331 | $this->logger->error('Cannot read body', ['exception' => $e]);
332 | throw new RequestFailException('Cannot read body', 0, $e);
333 | } catch (InvalidArgumentException $e) {
334 | $this->logger->error('Wrong header', ['exception' => $e]);
335 | throw new RequestFailException('Wrong header', 0, $e);
336 | }
337 | }
338 |
339 | private function getTimeStamp(): string
340 | {
341 | return date('Y.m.d H:i:s O');
342 | }
343 |
344 | /**
345 | * Generate state with uuid
346 | *
347 | * @throws SignFailException
348 | */
349 | private function buildState(): string
350 | {
351 | try {
352 | return sprintf(
353 | '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
354 | random_int(0, 0xffff),
355 | random_int(0, 0xffff),
356 | random_int(0, 0xffff),
357 | random_int(0, 0x0fff) | 0x4000,
358 | random_int(0, 0x3fff) | 0x8000,
359 | random_int(0, 0xffff),
360 | random_int(0, 0xffff),
361 | random_int(0, 0xffff)
362 | );
363 | } catch (Exception $e) {
364 | throw new CannotGenerateRandomIntException('Cannot generate random integer', $e);
365 | }
366 | }
367 |
368 | /**
369 | * Url safe for base64
370 | */
371 | private function base64UrlSafeDecode(string $string): string
372 | {
373 | $base64 = strtr($string, '-_', '+/');
374 |
375 | return base64_decode($base64);
376 | }
377 | }
378 |
--------------------------------------------------------------------------------
/src/Esia/Signer/AbstractSignerPKCS7.php:
--------------------------------------------------------------------------------
1 | certPath = $certPath;
49 | $this->privateKeyPath = $privateKeyPath;
50 | $this->privateKeyPassword = $privateKeyPassword;
51 | $this->tmpPath = $tmpPath;
52 | $this->logger = new NullLogger();
53 | }
54 |
55 | /**
56 | * Temporary directory for message signing (must me writable)
57 | *
58 | * @var string
59 | */
60 | protected $tmpPath;
61 |
62 | /**
63 | * @throws SignFailException
64 | */
65 | protected function checkFilesExists(): void
66 | {
67 | if (!file_exists($this->certPath)) {
68 | throw new NoSuchCertificateFileException('Certificate does not exist');
69 | }
70 | if (!is_readable($this->certPath)) {
71 | throw new CannotReadCertificateException('Cannot read the certificate');
72 | }
73 | if (!file_exists($this->privateKeyPath)) {
74 | throw new NoSuchKeyFileException('Private key does not exist');
75 | }
76 | if (!is_readable($this->privateKeyPath)) {
77 | throw new CannotReadPrivateKeyException('Cannot read the private key');
78 | }
79 | if (!file_exists($this->tmpPath)) {
80 | throw new NoSuchTmpDirException('Temporary folder is not found');
81 | }
82 | if (!is_writable($this->tmpPath)) {
83 | throw new NoSuchTmpDirException('Temporary folder is not writable');
84 | }
85 | }
86 |
87 | /**
88 | * Generate random unique string
89 | */
90 | protected function getRandomString(): string
91 | {
92 | return md5(uniqid(mt_rand(), true));
93 | }
94 |
95 | /**
96 | * Url safe for base64
97 | */
98 | protected function urlSafe(string $string): string
99 | {
100 | return rtrim(strtr(trim($string), '+/', '-_'), '=');
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/Esia/Signer/CliSignerPKCS7.php:
--------------------------------------------------------------------------------
1 | checkFilesExists();
15 |
16 | // random unique directories for sign
17 | $messageFile = $this->tmpPath . DIRECTORY_SEPARATOR . $this->getRandomString();
18 | $signFile = $this->tmpPath . DIRECTORY_SEPARATOR . $this->getRandomString();
19 | file_put_contents($messageFile, $message);
20 |
21 | $this->run(
22 | 'openssl ' .
23 | 'smime -engine gost -sign -binary -outform DER -noattr ' .
24 | '-signer ' . escapeshellarg($this->certPath) . ' ' .
25 | '-inkey ' . escapeshellarg($this->privateKeyPath) . ' ' .
26 | '-passin ' . escapeshellarg('pass:' . $this->privateKeyPassword) . ' ' .
27 | '-in ' . escapeshellarg($messageFile) . ' ' .
28 | '-out ' . escapeshellarg($signFile)
29 | );
30 |
31 | $signed = file_get_contents($signFile);
32 | if ($signed === false) {
33 | $message = sprintf('cannot read %s file', $signFile);
34 | $this->logger->error($message);
35 | throw new SignFailException($message);
36 | }
37 | $sign = $this->urlSafe(base64_encode($signed));
38 |
39 | unlink($signFile);
40 | unlink($messageFile);
41 | return $sign;
42 | }
43 |
44 | /**
45 | * @throws SignFailException
46 | */
47 | private function run(string $command): void
48 | {
49 | $process = proc_open(
50 | $command,
51 | [
52 | ['pipe', 'w'], // stdout
53 | ['pipe', 'w'], // stderr
54 | ],
55 | $pipes
56 | );
57 |
58 | $result = stream_get_contents($pipes[0]);
59 | fclose($pipes[0]);
60 |
61 | $errors = stream_get_contents($pipes[1]);
62 | fclose($pipes[1]);
63 |
64 | $code = proc_close($process);
65 | if (0 !== $code || $result === false) {
66 | $errors = $errors ?: 'unknown';
67 | $this->logger->error('Sign fail');
68 | $this->logger->error('SSL error: ' . $errors);
69 | throw new SignFailException($errors);
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Esia/Signer/Exceptions/CannotGenerateRandomIntException.php:
--------------------------------------------------------------------------------
1 | pkcs7Flags |= $pkcs7Flag;
16 | }
17 |
18 | /**
19 | * @throws SignFailException
20 | */
21 | public function sign(string $message): string
22 | {
23 | $this->checkFilesExists();
24 |
25 | $certContent = file_get_contents($this->certPath);
26 | $keyContent = file_get_contents($this->privateKeyPath);
27 |
28 | $cert = openssl_x509_read($certContent);
29 |
30 | if ($cert === false) {
31 | throw new CannotReadCertificateException('Cannot read the certificate: ' . openssl_error_string());
32 | }
33 |
34 | $this->logger->debug('Cert: ' . print_r($cert, true), ['cert' => $cert]);
35 |
36 | $privateKey = openssl_pkey_get_private($keyContent, $this->privateKeyPassword);
37 |
38 | if ($privateKey === false) {
39 | throw new CannotReadPrivateKeyException('Cannot read the private key: ' . openssl_error_string());
40 | }
41 |
42 | $this->logger->debug('Private key: : ' . print_r($privateKey, true), ['privateKey' => $privateKey]);
43 |
44 | // random unique directories for sign
45 | $messageFile = $this->tmpPath . DIRECTORY_SEPARATOR . $this->getRandomString();
46 | $signFile = $this->tmpPath . DIRECTORY_SEPARATOR . $this->getRandomString();
47 | file_put_contents($messageFile, $message);
48 | $signResult = openssl_pkcs7_sign(
49 | $messageFile,
50 | $signFile,
51 | $cert,
52 | $privateKey,
53 | [],
54 | $this->pkcs7Flags
55 | );
56 |
57 | if ($signResult) {
58 | $this->logger->debug('Sign success');
59 | } else {
60 | $this->logger->error('Sign fail');
61 | $this->logger->error('SSL error: ' . openssl_error_string());
62 | throw new SignFailException('Cannot sign the message');
63 | }
64 |
65 | $signed = file_get_contents($signFile);
66 |
67 | # split by section
68 | $signed = explode("\n\n", $signed);
69 |
70 | # get third section which contains sign and join into one line
71 | $sign = str_replace("\n", '', $this->urlSafe($signed[3]));
72 |
73 | unlink($signFile);
74 | unlink($messageFile);
75 |
76 | return $sign;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/tests/.configure-gost-openssl.sh:
--------------------------------------------------------------------------------
1 | sed -i '1iopenssl_conf=openssl_def' /usr/lib/ssl/openssl.cnf
2 | tee -a /usr/lib/ssl/openssl.cnf <assertEquals(5, $element->getChildrenCount());
31 | * ```
32 | *
33 | * Floating-point example:
34 | * ```php
35 | * assertEquals(0.3, $calculator->add(0.1, 0.2), 'Calculator should add the two numbers correctly.', 0.01);
37 | * ```
38 | *
39 | * @param $expected
40 | * @param $actual
41 | * @param string $message
42 | * @param float $delta
43 | * @see \Codeception\Module\Asserts::assertEquals()
44 | */
45 | public function assertEquals($expected, $actual, $message = null, $delta = null) {
46 | return $this->getScenario()->runStep(new Action('assertEquals', func_get_args()));
47 | }
48 |
49 |
50 | /**
51 | * [!] Method is generated. Documentation taken from corresponding module.
52 | *
53 | * Checks that two variables are not equal. If you're comparing floating-point values,
54 | * you can specify the optional "delta" parameter which dictates how great of a precision
55 | * error are you willing to tolerate in order to consider the two values not equal.
56 | *
57 | * Regular example:
58 | * ```php
59 | * assertNotEquals(0, $element->getChildrenCount());
61 | * ```
62 | *
63 | * Floating-point example:
64 | * ```php
65 | * assertNotEquals(0.4, $calculator->add(0.1, 0.2), 'Calculator should add the two numbers correctly.', 0.01);
67 | * ```
68 | *
69 | * @param $expected
70 | * @param $actual
71 | * @param string $message
72 | * @param float $delta
73 | * @see \Codeception\Module\Asserts::assertNotEquals()
74 | */
75 | public function assertNotEquals($expected, $actual, $message = null, $delta = null) {
76 | return $this->getScenario()->runStep(new Action('assertNotEquals', func_get_args()));
77 | }
78 |
79 |
80 | /**
81 | * [!] Method is generated. Documentation taken from corresponding module.
82 | *
83 | * Checks that two variables are same
84 | *
85 | * @param $expected
86 | * @param $actual
87 | * @param string $message
88 | * @see \Codeception\Module\Asserts::assertSame()
89 | */
90 | public function assertSame($expected, $actual, $message = null) {
91 | return $this->getScenario()->runStep(new Action('assertSame', func_get_args()));
92 | }
93 |
94 |
95 | /**
96 | * [!] Method is generated. Documentation taken from corresponding module.
97 | *
98 | * Checks that two variables are not same
99 | *
100 | * @param $expected
101 | * @param $actual
102 | * @param string $message
103 | * @see \Codeception\Module\Asserts::assertNotSame()
104 | */
105 | public function assertNotSame($expected, $actual, $message = null) {
106 | return $this->getScenario()->runStep(new Action('assertNotSame', func_get_args()));
107 | }
108 |
109 |
110 | /**
111 | * [!] Method is generated. Documentation taken from corresponding module.
112 | *
113 | * Checks that actual is greater than expected
114 | *
115 | * @param $expected
116 | * @param $actual
117 | * @param string $message
118 | * @see \Codeception\Module\Asserts::assertGreaterThan()
119 | */
120 | public function assertGreaterThan($expected, $actual, $message = null) {
121 | return $this->getScenario()->runStep(new Action('assertGreaterThan', func_get_args()));
122 | }
123 |
124 |
125 | /**
126 | * [!] Method is generated. Documentation taken from corresponding module.
127 | *
128 | * Checks that actual is greater or equal than expected
129 | *
130 | * @param $expected
131 | * @param $actual
132 | * @param string $message
133 | * @see \Codeception\Module\Asserts::assertGreaterThanOrEqual()
134 | */
135 | public function assertGreaterThanOrEqual($expected, $actual, $message = null) {
136 | return $this->getScenario()->runStep(new Action('assertGreaterThanOrEqual', func_get_args()));
137 | }
138 |
139 |
140 | /**
141 | * [!] Method is generated. Documentation taken from corresponding module.
142 | *
143 | * Checks that actual is less than expected
144 | *
145 | * @param $expected
146 | * @param $actual
147 | * @param string $message
148 | * @see \Codeception\Module\Asserts::assertLessThan()
149 | */
150 | public function assertLessThan($expected, $actual, $message = null) {
151 | return $this->getScenario()->runStep(new Action('assertLessThan', func_get_args()));
152 | }
153 |
154 |
155 | /**
156 | * [!] Method is generated. Documentation taken from corresponding module.
157 | *
158 | * Checks that actual is less or equal than expected
159 | *
160 | * @param $expected
161 | * @param $actual
162 | * @param string $message
163 | * @see \Codeception\Module\Asserts::assertLessThanOrEqual()
164 | */
165 | public function assertLessThanOrEqual($expected, $actual, $message = null) {
166 | return $this->getScenario()->runStep(new Action('assertLessThanOrEqual', func_get_args()));
167 | }
168 |
169 |
170 | /**
171 | * [!] Method is generated. Documentation taken from corresponding module.
172 | *
173 | * Checks that haystack contains needle
174 | *
175 | * @param $needle
176 | * @param $haystack
177 | * @param string $message
178 | * @see \Codeception\Module\Asserts::assertContains()
179 | */
180 | public function assertContains($needle, $haystack, $message = null) {
181 | return $this->getScenario()->runStep(new Action('assertContains', func_get_args()));
182 | }
183 |
184 |
185 | /**
186 | * [!] Method is generated. Documentation taken from corresponding module.
187 | *
188 | * Checks that haystack doesn't contain needle.
189 | *
190 | * @param $needle
191 | * @param $haystack
192 | * @param string $message
193 | * @see \Codeception\Module\Asserts::assertNotContains()
194 | */
195 | public function assertNotContains($needle, $haystack, $message = null) {
196 | return $this->getScenario()->runStep(new Action('assertNotContains', func_get_args()));
197 | }
198 |
199 |
200 | /**
201 | * [!] Method is generated. Documentation taken from corresponding module.
202 | *
203 | * Checks that string match with pattern
204 | *
205 | * @param string $pattern
206 | * @param string $string
207 | * @param string $message
208 | * @see \Codeception\Module\Asserts::assertRegExp()
209 | */
210 | public function assertRegExp($pattern, $string, $message = null) {
211 | return $this->getScenario()->runStep(new Action('assertRegExp', func_get_args()));
212 | }
213 |
214 |
215 | /**
216 | * [!] Method is generated. Documentation taken from corresponding module.
217 | *
218 | * Checks that string not match with pattern
219 | *
220 | * @param string $pattern
221 | * @param string $string
222 | * @param string $message
223 | * @see \Codeception\Module\Asserts::assertNotRegExp()
224 | */
225 | public function assertNotRegExp($pattern, $string, $message = null) {
226 | return $this->getScenario()->runStep(new Action('assertNotRegExp', func_get_args()));
227 | }
228 |
229 |
230 | /**
231 | * [!] Method is generated. Documentation taken from corresponding module.
232 | *
233 | * Checks that a string starts with the given prefix.
234 | *
235 | * @param string $prefix
236 | * @param string $string
237 | * @param string $message
238 | * @see \Codeception\Module\Asserts::assertStringStartsWith()
239 | */
240 | public function assertStringStartsWith($prefix, $string, $message = null) {
241 | return $this->getScenario()->runStep(new Action('assertStringStartsWith', func_get_args()));
242 | }
243 |
244 |
245 | /**
246 | * [!] Method is generated. Documentation taken from corresponding module.
247 | *
248 | * Checks that a string doesn't start with the given prefix.
249 | *
250 | * @param string $prefix
251 | * @param string $string
252 | * @param string $message
253 | * @see \Codeception\Module\Asserts::assertStringStartsNotWith()
254 | */
255 | public function assertStringStartsNotWith($prefix, $string, $message = null) {
256 | return $this->getScenario()->runStep(new Action('assertStringStartsNotWith', func_get_args()));
257 | }
258 |
259 |
260 | /**
261 | * [!] Method is generated. Documentation taken from corresponding module.
262 | *
263 | * Checks that variable is empty.
264 | *
265 | * @param $actual
266 | * @param string $message
267 | * @see \Codeception\Module\Asserts::assertEmpty()
268 | */
269 | public function assertEmpty($actual, $message = null) {
270 | return $this->getScenario()->runStep(new Action('assertEmpty', func_get_args()));
271 | }
272 |
273 |
274 | /**
275 | * [!] Method is generated. Documentation taken from corresponding module.
276 | *
277 | * Checks that variable is not empty.
278 | *
279 | * @param $actual
280 | * @param string $message
281 | * @see \Codeception\Module\Asserts::assertNotEmpty()
282 | */
283 | public function assertNotEmpty($actual, $message = null) {
284 | return $this->getScenario()->runStep(new Action('assertNotEmpty', func_get_args()));
285 | }
286 |
287 |
288 | /**
289 | * [!] Method is generated. Documentation taken from corresponding module.
290 | *
291 | * Checks that variable is NULL
292 | *
293 | * @param $actual
294 | * @param string $message
295 | * @see \Codeception\Module\Asserts::assertNull()
296 | */
297 | public function assertNull($actual, $message = null) {
298 | return $this->getScenario()->runStep(new Action('assertNull', func_get_args()));
299 | }
300 |
301 |
302 | /**
303 | * [!] Method is generated. Documentation taken from corresponding module.
304 | *
305 | * Checks that variable is not NULL
306 | *
307 | * @param $actual
308 | * @param string $message
309 | * @see \Codeception\Module\Asserts::assertNotNull()
310 | */
311 | public function assertNotNull($actual, $message = null) {
312 | return $this->getScenario()->runStep(new Action('assertNotNull', func_get_args()));
313 | }
314 |
315 |
316 | /**
317 | * [!] Method is generated. Documentation taken from corresponding module.
318 | *
319 | * Checks that condition is positive.
320 | *
321 | * @param $condition
322 | * @param string $message
323 | * @see \Codeception\Module\Asserts::assertTrue()
324 | */
325 | public function assertTrue($condition, $message = null)
326 | {
327 | return $this->getScenario()->runStep(new Action('assertTrue', func_get_args()));
328 | }
329 |
330 |
331 | /**
332 | * [!] Method is generated. Documentation taken from corresponding module.
333 | *
334 | * Checks that the condition is NOT true (everything but true)
335 | *
336 | * @param $condition
337 | * @param string $message
338 | * @see \Codeception\Module\Asserts::assertNotTrue()
339 | */
340 | public function assertNotTrue($condition, $message = null)
341 | {
342 | return $this->getScenario()->runStep(new Action('assertNotTrue', func_get_args()));
343 | }
344 |
345 |
346 | /**
347 | * [!] Method is generated. Documentation taken from corresponding module.
348 | *
349 | * Checks that condition is negative.
350 | *
351 | * @param $condition
352 | * @param string $message
353 | * @see \Codeception\Module\Asserts::assertFalse()
354 | */
355 | public function assertFalse($condition, $message = null)
356 | {
357 | return $this->getScenario()->runStep(new Action('assertFalse', func_get_args()));
358 | }
359 |
360 |
361 | /**
362 | * [!] Method is generated. Documentation taken from corresponding module.
363 | *
364 | * Checks that the condition is NOT false (everything but false)
365 | *
366 | * @param $condition
367 | * @param string $message
368 | * @see \Codeception\Module\Asserts::assertNotFalse()
369 | */
370 | public function assertNotFalse($condition, $message = null)
371 | {
372 | return $this->getScenario()->runStep(new Action('assertNotFalse', func_get_args()));
373 | }
374 |
375 |
376 | /**
377 | * [!] Method is generated. Documentation taken from corresponding module.
378 | *
379 | * Checks if file exists
380 | *
381 | * @param string $filename
382 | * @param string $message
383 | * @see \Codeception\Module\Asserts::assertFileExists()
384 | */
385 | public function assertFileExists($filename, $message = null)
386 | {
387 | return $this->getScenario()->runStep(new Action('assertFileExists', func_get_args()));
388 | }
389 |
390 |
391 | /**
392 | * [!] Method is generated. Documentation taken from corresponding module.
393 | *
394 | * Checks if file doesn't exist
395 | *
396 | * @param string $filename
397 | * @param string $message
398 | * @see \Codeception\Module\Asserts::assertFileNotExists()
399 | */
400 | public function assertFileNotExists($filename, $message = null) {
401 | return $this->getScenario()->runStep(new Action('assertFileNotExists', func_get_args()));
402 | }
403 |
404 |
405 | /**
406 | * [!] Method is generated. Documentation taken from corresponding module.
407 | *
408 | * @param $expected
409 | * @param $actual
410 | * @param $description
411 | * @see \Codeception\Module\Asserts::assertGreaterOrEquals()
412 | */
413 | public function assertGreaterOrEquals($expected, $actual, $description = null) {
414 | return $this->getScenario()->runStep(new Action('assertGreaterOrEquals', func_get_args()));
415 | }
416 |
417 |
418 | /**
419 | * [!] Method is generated. Documentation taken from corresponding module.
420 | *
421 | * @param $expected
422 | * @param $actual
423 | * @param $description
424 | * @see \Codeception\Module\Asserts::assertLessOrEquals()
425 | */
426 | public function assertLessOrEquals($expected, $actual, $description = null) {
427 | return $this->getScenario()->runStep(new Action('assertLessOrEquals', func_get_args()));
428 | }
429 |
430 |
431 | /**
432 | * [!] Method is generated. Documentation taken from corresponding module.
433 | *
434 | * @param $actual
435 | * @param $description
436 | * @see \Codeception\Module\Asserts::assertIsEmpty()
437 | */
438 | public function assertIsEmpty($actual, $description = null) {
439 | return $this->getScenario()->runStep(new Action('assertIsEmpty', func_get_args()));
440 | }
441 |
442 |
443 | /**
444 | * [!] Method is generated. Documentation taken from corresponding module.
445 | *
446 | * @param $key
447 | * @param $actual
448 | * @param $description
449 | * @see \Codeception\Module\Asserts::assertArrayHasKey()
450 | */
451 | public function assertArrayHasKey($key, $actual, $description = null) {
452 | return $this->getScenario()->runStep(new Action('assertArrayHasKey', func_get_args()));
453 | }
454 |
455 |
456 | /**
457 | * [!] Method is generated. Documentation taken from corresponding module.
458 | *
459 | * @param $key
460 | * @param $actual
461 | * @param $description
462 | * @see \Codeception\Module\Asserts::assertArrayNotHasKey()
463 | */
464 | public function assertArrayNotHasKey($key, $actual, $description = null) {
465 | return $this->getScenario()->runStep(new Action('assertArrayNotHasKey', func_get_args()));
466 | }
467 |
468 |
469 | /**
470 | * [!] Method is generated. Documentation taken from corresponding module.
471 | *
472 | * Checks that array contains subset.
473 | *
474 | * @param array $subset
475 | * @param array $array
476 | * @param bool $strict
477 | * @param string $message
478 | * @see \Codeception\Module\Asserts::assertArraySubset()
479 | */
480 | public function assertArraySubset($subset, $array, $strict = null, $message = null) {
481 | return $this->getScenario()->runStep(new Action('assertArraySubset', func_get_args()));
482 | }
483 |
484 |
485 | /**
486 | * [!] Method is generated. Documentation taken from corresponding module.
487 | *
488 | * @param $expectedCount
489 | * @param $actual
490 | * @param $description
491 | * @see \Codeception\Module\Asserts::assertCount()
492 | */
493 | public function assertCount($expectedCount, $actual, $description = null) {
494 | return $this->getScenario()->runStep(new Action('assertCount', func_get_args()));
495 | }
496 |
497 |
498 | /**
499 | * [!] Method is generated. Documentation taken from corresponding module.
500 | *
501 | * @param $class
502 | * @param $actual
503 | * @param $description
504 | * @see \Codeception\Module\Asserts::assertInstanceOf()
505 | */
506 | public function assertInstanceOf($class, $actual, $description = null) {
507 | return $this->getScenario()->runStep(new Action('assertInstanceOf', func_get_args()));
508 | }
509 |
510 |
511 | /**
512 | * [!] Method is generated. Documentation taken from corresponding module.
513 | *
514 | * @param $class
515 | * @param $actual
516 | * @param $description
517 | * @see \Codeception\Module\Asserts::assertNotInstanceOf()
518 | */
519 | public function assertNotInstanceOf($class, $actual, $description = null) {
520 | return $this->getScenario()->runStep(new Action('assertNotInstanceOf', func_get_args()));
521 | }
522 |
523 |
524 | /**
525 | * [!] Method is generated. Documentation taken from corresponding module.
526 | *
527 | * @param $type
528 | * @param $actual
529 | * @param $description
530 | * @see \Codeception\Module\Asserts::assertInternalType()
531 | */
532 | public function assertInternalType($type, $actual, $description = null) {
533 | return $this->getScenario()->runStep(new Action('assertInternalType', func_get_args()));
534 | }
535 |
536 |
537 | /**
538 | * [!] Method is generated. Documentation taken from corresponding module.
539 | *
540 | * Fails the test with message.
541 | *
542 | * @param $message
543 | * @see \Codeception\Module\Asserts::fail()
544 | */
545 | public function fail($message) {
546 | return $this->getScenario()->runStep(new Action('fail', func_get_args()));
547 | }
548 |
549 |
550 | /**
551 | * [!] Method is generated. Documentation taken from corresponding module.
552 | *
553 | * Handles and checks exception called inside callback function.
554 | * Either exception class name or exception instance should be provided.
555 | *
556 | * ```php
557 | * expectException(MyException::class, function() {
559 | * $this->doSomethingBad();
560 | * });
561 | *
562 | * $I->expectException(new MyException(), function() {
563 | * $this->doSomethingBad();
564 | * });
565 | * ```
566 | * If you want to check message or exception code, you can pass them with exception instance:
567 | * ```php
568 | * expectException(new MyException("Don't do bad things"), function() {
571 | * $this->doSomethingBad();
572 | * });
573 | * ```
574 | *
575 | * @param $exception string or \Exception
576 | * @param $callback
577 | *
578 | * @deprecated Use expectThrowable instead
579 | * @see \Codeception\Module\Asserts::expectException()
580 | */
581 | public function expectException($exception, $callback)
582 | {
583 | return $this->getScenario()->runStep(new Action('expectException', func_get_args()));
584 | }
585 |
586 |
587 | /**
588 | * [!] Method is generated. Documentation taken from corresponding module.
589 | *
590 | * Handles and checks throwables (Exceptions/Errors) called inside the callback function.
591 | * Either throwable class name or throwable instance should be provided.
592 | *
593 | * ```php
594 | * expectThrowable(MyThrowable::class, function() {
596 | * $this->doSomethingBad();
597 | * });
598 | *
599 | * $I->expectThrowable(new MyException(), function() {
600 | * $this->doSomethingBad();
601 | * });
602 | * ```
603 | * If you want to check message or throwable code, you can pass them with throwable instance:
604 | * ```php
605 | * expectThrowable(new MyError("Don't do bad things"), function() {
608 | * $this->doSomethingBad();
609 | * });
610 | * ```
611 | *
612 | * @param $throwable string or \Throwable
613 | * @param $callback
614 | * @see \Codeception\Module\Asserts::expectThrowable()
615 | */
616 | public function expectThrowable($throwable, $callback)
617 | {
618 | return $this->getScenario()->runStep(new Action('expectThrowable', func_get_args()));
619 | }
620 | }
621 |
--------------------------------------------------------------------------------
/tests/unit.suite.yml:
--------------------------------------------------------------------------------
1 | # Codeception Test Suite Configuration
2 | #
3 | # Suite for unit (internal) tests.
4 |
5 | class_name: UnitTester
6 | modules:
7 | enabled:
8 | - Asserts
9 | - \Helper\Unit
--------------------------------------------------------------------------------
/tests/unit/ConfigTest.php:
--------------------------------------------------------------------------------
1 | 'test',
25 | 'redirectUrl' => 'http://google.com',
26 | 'privateKeyPath' => '/tmp',
27 | 'certPath' => '/tmp',
28 | 'scope' => ['test', 'test2', 'test3'],
29 | ]);
30 |
31 | $this->assertSame('test test2 test3', $config->getScopeString());
32 | }
33 |
34 | /**
35 | * Data provider for @see ConfigTest::testConstruct()
36 | *
37 | * @return array
38 | */
39 | public function dataProviderForConstructor(): array
40 | {
41 | return [
42 | 'min' => [
43 | [
44 | 'clientId' => 'test',
45 | 'redirectUrl' => 'http://google.com',
46 | 'privateKeyPath' => '/tmp',
47 | 'certPath' => '/tmp',
48 | 'scope' => ['test', 'test2', 'test3'],
49 | ],
50 | null,
51 | ],
52 | 'max' => [
53 | [
54 | 'clientId' => 'test',
55 | 'redirectUrl' => 'http://google.com',
56 | 'privateKeyPath' => '/tmp',
57 | 'certPath' => '/tmp',
58 | 'portalUrl' => 'google.com',
59 | 'tokenUrlPath' => 'test',
60 | 'codeUrlPath' => 'test',
61 | 'personUrlPath' => 'test',
62 | 'logoutUrlPath' => 'test',
63 | 'privateKeyPassword' => 'test',
64 | 'oid' => 'test',
65 | 'responseType' => 'test',
66 | 'accessType' => 'test',
67 | 'tmpPath' => 'test',
68 | 'token' => 'test',
69 | 'scope' => ['test', 'test2', 'test3'],
70 | ],
71 | null,
72 | ],
73 | 'No cert path' => [
74 | [
75 | 'clientId' => 'test',
76 | 'redirectUrl' => 'http://google.com',
77 | 'privateKeyPath' => '/tmp',
78 | 'scope' => ['test', 'test2', 'test3'],
79 | ],
80 | InvalidConfigurationException::class,
81 | ],
82 | 'No private key path' => [
83 | [
84 | 'clientId' => 'test',
85 | 'redirectUrl' => 'http://google.com',
86 | 'certPath' => '/tmp',
87 | 'scope' => ['test', 'test2', 'test3'],
88 | ],
89 | InvalidConfigurationException::class,
90 | ],
91 | 'No redirect url' => [
92 | [
93 | 'clientId' => 'test',
94 | 'privateKeyPath' => '/tmp',
95 | 'certPath' => '/tmp',
96 | 'scope' => ['test', 'test2', 'test3'],
97 | ],
98 | InvalidConfigurationException::class,
99 | ],
100 | 'No client id' => [
101 | [
102 | 'redirectUrl' => 'http://google.com',
103 | 'privateKeyPath' => '/tmp',
104 | 'certPath' => '/tmp',
105 | 'scope' => ['test', 'test2', 'test3'],
106 | ],
107 | InvalidConfigurationException::class,
108 | ],
109 | 'invalid scope' => [
110 | [
111 | 'redirectUrl' => 'http://google.com',
112 | 'privateKeyPath' => '/tmp',
113 | 'certPath' => '/tmp',
114 | 'scope' => 'test test2 test3',
115 | ],
116 | InvalidConfigurationException::class,
117 | ],
118 | ];
119 | }
120 |
121 | /**
122 | * @param $config
123 | * @param string|null $expectedException
124 | * @throws \Esia\Exceptions\InvalidConfigurationException
125 | *
126 | * @dataProvider dataProviderForConstructor
127 | */
128 | public function testConstruct($config, string $expectedException = null): void
129 | {
130 | if ($expectedException) {
131 | $this->expectException($expectedException);
132 | }
133 |
134 | new Config($config);
135 | }
136 |
137 | /**
138 | * @throws InvalidConfigurationException
139 | */
140 | public function testGetTokenUrl(): void
141 | {
142 | $config = new Config([
143 | 'clientId' => 'test',
144 | 'redirectUrl' => 'http://google.com',
145 | 'privateKeyPath' => '/tmp',
146 | 'certPath' => '/tmp',
147 | 'portalUrl' => 'https://google.com/',
148 | 'tokenUrlPath' => 'test',
149 | 'scope' => ['test', 'test2', 'test3'],
150 | ]);
151 |
152 | $this->assertSame('https://google.com/test', $config->getTokenUrl());
153 | }
154 |
155 | /**
156 | * @throws InvalidConfigurationException
157 | */
158 | public function testGetCodeUrl(): void
159 | {
160 | $config = new Config([
161 | 'clientId' => 'test',
162 | 'redirectUrl' => 'http://google.com',
163 | 'privateKeyPath' => '/tmp',
164 | 'certPath' => '/tmp',
165 | 'portalUrl' => 'https://google.com/',
166 | 'codeUrlPath' => 'test',
167 | 'scope' => ['test', 'test2', 'test3'],
168 | ]);
169 |
170 | $this->assertSame('https://google.com/test', $config->getCodeUrl());
171 | }
172 |
173 | /**
174 | * @throws InvalidConfigurationException
175 | */
176 | public function testGetPersonUrl(): void
177 | {
178 | $config = new Config([
179 | 'clientId' => 'test',
180 | 'redirectUrl' => 'http://google.com',
181 | 'privateKeyPath' => '/tmp',
182 | 'certPath' => '/tmp',
183 | 'portalUrl' => 'https://google.com/',
184 | 'personUrlPath' => 'test',
185 | 'oid' => 'test',
186 | 'scope' => ['test', 'test2', 'test3'],
187 | ]);
188 |
189 | $this->assertSame('https://google.com/test/test', $config->getPersonUrl());
190 | }
191 | /**
192 | * @throws InvalidConfigurationException
193 | */
194 | public function testGetPersonUrlWithoutOid(): void
195 | {
196 | $config = new Config([
197 | 'clientId' => 'test',
198 | 'redirectUrl' => 'http://google.com',
199 | 'privateKeyPath' => '/tmp',
200 | 'certPath' => '/tmp',
201 | 'portalUrl' => 'https://google.com/',
202 | 'personUrlPath' => 'test',
203 | 'scope' => ['test', 'test2', 'test3'],
204 | ]);
205 |
206 | $this->expectException(InvalidConfigurationException::class);
207 | $this->assertSame('https://google.com/test/test', $config->getPersonUrl());
208 | }
209 | /**
210 | * @throws InvalidConfigurationException
211 | */
212 | public function testGetLogoutUrl(): void
213 | {
214 | $config = new Config([
215 | 'clientId' => 'test',
216 | 'redirectUrl' => 'http://google.com',
217 | 'privateKeyPath' => '/tmp',
218 | 'certPath' => '/tmp',
219 | 'portalUrl' => 'https://google.com/',
220 | 'logoutUrlPath' => 'test',
221 | 'scope' => ['test', 'test2', 'test3'],
222 | ]);
223 |
224 | $this->assertSame('https://google.com/test', $config->getLogoutUrl());
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/tests/unit/Http/GuzzleHttpClientTest.php:
--------------------------------------------------------------------------------
1 | $handler]);
36 |
37 | $client = new GuzzleHttpClient($guzzleClient);
38 |
39 | $response = $client->sendRequest(new Request('GET', '/'));
40 |
41 | self::assertSame(200, $response->getStatusCode());
42 |
43 | $this->expectException(ClientExceptionInterface::class);
44 | $client->sendRequest(new Request('GET', '/'));
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/tests/unit/OpenIdCliOpensslTest.php:
--------------------------------------------------------------------------------
1 | config = [
20 | 'clientId' => 'INSP03211',
21 | 'redirectUrl' => 'http://my-site.com/response.php',
22 | 'portalUrl' => 'https://esia-portal1.test.gosuslugi.ru/',
23 | 'privateKeyPath' => codecept_data_dir('server-gost.key'),
24 | 'privateKeyPassword' => 'test',
25 | 'certPath' => codecept_data_dir('server-gost.crt'),
26 | 'tmpPath' => codecept_log_dir(),
27 | ];
28 |
29 | $config = new Config($this->config);
30 |
31 | $this->openId = new OpenId($config);
32 | $this->openId->setSigner(new CliSignerPKCS7(
33 | $this->config['certPath'],
34 | $this->config['privateKeyPath'],
35 | $this->config['privateKeyPassword'],
36 | $this->config['tmpPath']
37 | ));
38 | }
39 |
40 | /**
41 | * @throws AbstractEsiaException
42 | * @throws InvalidConfigurationException
43 | */
44 | public function testGetToken(): void
45 | {
46 | $config = new Config($this->config);
47 |
48 | $oid = '123';
49 | $oidBase64 = base64_encode('{ "urn:esia:sbj_id" : ' . $oid . '}');
50 |
51 | $client = $this->buildClientWithResponses([
52 | new Response(200, [], '{ "access_token": "test.' . $oidBase64 . '.test"}'),
53 | ]);
54 | $openId = new OpenId($config, $client);
55 | $openId->setSigner(new CliSignerPKCS7(
56 | $this->config['certPath'],
57 | $this->config['privateKeyPath'],
58 | $this->config['privateKeyPassword'],
59 | $this->config['tmpPath']
60 | ));
61 | $token = $openId->getToken('test');
62 | self::assertNotEmpty($token);
63 | self::assertSame($oid, $openId->getConfig()->getOid());
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/tests/unit/OpenIdTest.php:
--------------------------------------------------------------------------------
1 | config = [
33 | 'clientId' => 'INSP03211',
34 | 'redirectUrl' => 'http://my-site.com/response.php',
35 | 'portalUrl' => 'https://esia-portal1.test.gosuslugi.ru/',
36 | 'privateKeyPath' => codecept_data_dir('server.key'),
37 | 'privateKeyPassword' => 'test',
38 | 'certPath' => codecept_data_dir('server.crt'),
39 | 'tmpPath' => codecept_log_dir(),
40 | ];
41 |
42 | $config = new Config($this->config);
43 |
44 | $this->openId = new OpenId($config);
45 | }
46 |
47 | /**
48 | * @throws SignFailException
49 | * @throws AbstractEsiaException
50 | * @throws InvalidConfigurationException
51 | */
52 | public function testGetToken(): void
53 | {
54 | $config = new Config($this->config);
55 |
56 | $oid = '123';
57 | $oidBase64 = base64_encode('{ "urn:esia:sbj_id" : ' . $oid . '}');
58 |
59 | $client = $this->buildClientWithResponses([
60 | new Response(200, [], '{ "access_token": "test.' . $oidBase64 . '.test"}'),
61 | ]);
62 | $openId = new OpenId($config, $client);
63 |
64 | $token = $openId->getToken('test');
65 | self::assertNotEmpty($token);
66 | self::assertSame($oid, $openId->getConfig()->getOid());
67 | }
68 |
69 | /**
70 | * @throws InvalidConfigurationException
71 | * @throws AbstractEsiaException
72 | */
73 | public function testGetPersonInfo(): void
74 | {
75 | $config = new Config($this->config);
76 | $oid = '123';
77 | $config->setOid($oid);
78 | $config->setToken('test');
79 |
80 | $client = $this->buildClientWithResponses([
81 | new Response(200, [], '{"username": "test"}'),
82 | ]);
83 | $openId = new OpenId($config, $client);
84 |
85 | $info = $openId->getPersonInfo();
86 | self::assertNotEmpty($info);
87 | self::assertSame(['username' => 'test'], $info);
88 | }
89 |
90 | /**
91 | * @throws InvalidConfigurationException
92 | * @throws AbstractEsiaException
93 | */
94 | public function testGetContactInfo(): void
95 | {
96 | $config = new Config($this->config);
97 | $oid = '123';
98 | $config->setOid($oid);
99 | $config->setToken('test');
100 |
101 | $client = $this->buildClientWithResponses([
102 | new Response(200, [], '{"size": 2, "elements": ["phone", "email"]}'),
103 | new Response(200, [], '{"phone": "555 555 555"}'),
104 | new Response(200, [], '{"email": "test@gmail.com"}'),
105 | ]);
106 | $openId = new OpenId($config, $client);
107 |
108 | $info = $openId->getContactInfo();
109 | self::assertNotEmpty($info);
110 | self::assertSame([['phone' => '555 555 555'], ['email' => 'test@gmail.com']], $info);
111 | }
112 |
113 | /**
114 | * @throws InvalidConfigurationException
115 | * @throws AbstractEsiaException
116 | */
117 | public function testGetAddressInfo(): void
118 | {
119 | $config = new Config($this->config);
120 | $oid = '123';
121 | $config->setOid($oid);
122 | $config->setToken('test');
123 |
124 | $client = $this->buildClientWithResponses([
125 | new Response(200, [], '{"size": 2, "elements": ["phone", "email"]}'),
126 | new Response(200, [], '{"phone": "555 555 555"}'),
127 | new Response(200, [], '{"email": "test@gmail.com"}'),
128 | ]);
129 | $openId = new OpenId($config, $client);
130 |
131 | $info = $openId->getAddressInfo();
132 | self::assertNotEmpty($info);
133 | self::assertSame([['phone' => '555 555 555'], ['email' => 'test@gmail.com']], $info);
134 | }
135 |
136 | /**
137 | * @throws InvalidConfigurationException
138 | * @throws AbstractEsiaException
139 | */
140 | public function testGetDocInfo(): void
141 | {
142 | $config = new Config($this->config);
143 | $oid = '123';
144 | $config->setOid($oid);
145 | $config->setToken('test');
146 |
147 | $client = $this->buildClientWithResponses([
148 | new Response(200, [], '{"size": 2, "elements": ["phone", "email"]}'),
149 | new Response(200, [], '{"phone": "555 555 555"}'),
150 | new Response(200, [], '{"email": "test@gmail.com"}'),
151 | ]);
152 | $openId = new OpenId($config, $client);
153 |
154 | $info = $openId->getDocInfo();
155 | self::assertNotEmpty($info);
156 | self::assertSame([['phone' => '555 555 555'], ['email' => 'test@gmail.com']], $info);
157 | }
158 |
159 | /**
160 | * @throws InvalidConfigurationException
161 | */
162 | public function testBuildLogoutUrl(): void
163 | {
164 | $config = $this->openId->getConfig();
165 |
166 | $url = $config->getLogoutUrl() . '?client_id=' . $config->getClientId();
167 | $logoutUrl = $this->openId->buildLogoutUrl();
168 | self::assertSame($url, $logoutUrl);
169 | }
170 |
171 | /**
172 | * @throws InvalidConfigurationException
173 | */
174 | public function testBuildLogoutUrlWithRedirect(): void
175 | {
176 | $config = $this->openId->getConfig();
177 |
178 | $redirectUrl = 'test.example.com';
179 | $url = $config->getLogoutUrl() . '?client_id=' . $config->getClientId() . '&redirect_url=' . $redirectUrl;
180 | $logoutUrl = $this->openId->buildLogoutUrl($redirectUrl);
181 | self::assertSame($url, $logoutUrl);
182 | }
183 |
184 | /**
185 | * Client with prepared responses
186 | *
187 | * @param array $responses
188 | * @return ClientInterface
189 | */
190 | protected function buildClientWithResponses(array $responses): ClientInterface
191 | {
192 | $mock = new MockHandler($responses);
193 |
194 | $handler = HandlerStack::create($mock);
195 | $guzzleClient = new Client(['handler' => $handler]);
196 |
197 | return new GuzzleHttpClient($guzzleClient);
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/tests/unit/Signer/SignerPKCS7Test.php:
--------------------------------------------------------------------------------
1 | sign('test');
34 | self::assertNotEmpty($sign);
35 | }
36 |
37 | /**
38 | * @throws SignFailException
39 | */
40 | public function testSignCertDoesNotExists(): void
41 | {
42 | $signer = new SignerPKCS7(
43 | '/test',
44 | codecept_data_dir('server.key'),
45 | 'test',
46 | codecept_log_dir()
47 | );
48 |
49 | $this->expectException(NoSuchCertificateFileException::class);
50 | $signer->sign('test');
51 | }
52 |
53 | /**
54 | * @throws SignFailException
55 | */
56 | public function testPrivateKeyDoesNotExists(): void
57 | {
58 | $signer = new SignerPKCS7(
59 | codecept_data_dir('server.crt'),
60 | '/test',
61 | 'test',
62 | codecept_log_dir()
63 | );
64 |
65 | $this->expectException(NoSuchKeyFileException::class);
66 | $signer->sign('test');
67 | }
68 |
69 | /**
70 | * @throws SignFailException
71 | */
72 | public function testTmpDirDoesNotExists(): void
73 | {
74 | $signer = new SignerPKCS7(
75 | codecept_data_dir('server.crt'),
76 | codecept_data_dir('server.key'),
77 | 'test',
78 | '/'
79 | );
80 |
81 | $this->expectException(NoSuchTmpDirException::class);
82 | $signer->sign('test');
83 | }
84 |
85 | /**
86 | * @throws SignFailException
87 | */
88 | public function testTmpDirIsNotWritable(): void
89 | {
90 | $signer = new SignerPKCS7(
91 | codecept_data_dir('server.crt'),
92 | codecept_data_dir('server.key'),
93 | 'test',
94 | codecept_log_dir('non_writable_directory')
95 | );
96 |
97 | $this->expectException(NoSuchTmpDirException::class);
98 | $signer->sign('test');
99 | }
100 |
101 | /**
102 | * @throws SignFailException
103 | */
104 | public function testCertificateIsNotReadable(): void
105 | {
106 | $signer = new SignerPKCS7(
107 | codecept_data_dir('non_readable_file'),
108 | codecept_data_dir('server.key'),
109 | 'test',
110 | codecept_log_dir()
111 | );
112 |
113 | $this->expectException(CannotReadCertificateException::class);
114 | $signer->sign('test');
115 | }
116 |
117 | /**
118 | * @throws SignFailException
119 | */
120 | public function testPrivateKeyIsNotReadable(): void
121 | {
122 | $signer = new SignerPKCS7(
123 | codecept_data_dir('server.crt'),
124 | codecept_data_dir('non_readable_file'),
125 | 'test',
126 | codecept_log_dir()
127 | );
128 |
129 | $this->expectException(CannotReadPrivateKeyException::class);
130 | $signer->sign('test');
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/tests/unit/_bootstrap.php:
--------------------------------------------------------------------------------
1 |