Skip to content

Commit 9eb64b1

Browse files
authored
Add Request Object support (#245)
* Introduce RequestParamsResolver * Add jwks client property * Add Request Object support --------- Co-authored-by: Marko Ivančić <[email protected]>
1 parent 483d954 commit 9eb64b1

File tree

75 files changed

+1210
-594
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+1210
-594
lines changed

UPGRADE.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@
2121
- Entity Identifier
2222
- Registration Types
2323
- Federation JWKS
24+
- Protocol JWKS
2425
- Improved AuthProc filter support
2526
- Support authproc filters that need to redirect and later resume processing
2627
- `consent` and `preprodwarning` are two authprocs that redirect for user interaction and are now supported
2728
- Uses SSP's ProcessingChain class for closer alignment with SAML IdP configuration.
2829
- Allows additional configuration of authprocs in the main `config.php` under key `authproc.oidc`
29-
- Authorization endpoint now also supports sending parameters using HTTP POST method, in addition to GET.
30+
- Authorization endpoint now also supports sending request parameters using HTTP POST method, in addition to GET.
31+
- Added support for passing request parameters as JWTs, specifically - passing a Request Object by Value:
32+
https://openid.net/specs/openid-connect-core-1_0.html#RequestObject
3033

3134
## New configuration options
3235

conformance-tests/basic-skips.json

-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,2 @@
11
[
2-
{
3-
"test-name": "oidcc-unsigned-request-object-supported-correctly-or-rejected-as-unsupported",
4-
"variant": "*",
5-
"configuration-filename": "*"
6-
}
72
]

conformance-tests/conformance-basic-ci.json

-24
Original file line numberDiff line numberDiff line change
@@ -213,30 +213,6 @@
213213
}
214214
]
215215
},
216-
"oidcc-ensure-request-object-with-redirect-uri": {
217-
"browser": [
218-
{
219-
"comment": "expect an immediate error page",
220-
"match": "https://op.local.stack-dev.cirrusidentity.com/simplesaml/module.php/oidc/authorization*",
221-
"tasks": [
222-
{
223-
"task": "Expect redirect uri mismatch error page",
224-
"match": "https://op.local.stack-dev.cirrusidentity.com/simplesaml/module.php/oidc/authorization*",
225-
"commands": [
226-
[
227-
"wait",
228-
"xpath",
229-
"//*",
230-
10,
231-
"Check the `redirect_uri` parameter",
232-
"update-image-placeholder"
233-
]
234-
]
235-
}
236-
]
237-
}
238-
]
239-
},
240216
"oidcc-ensure-redirect-uri-in-authorization-request": {
241217
"browser": [
242218
{

conformance-tests/conformance-implicit-ci.json

-24
Original file line numberDiff line numberDiff line change
@@ -267,30 +267,6 @@
267267
}
268268
]
269269
},
270-
"oidcc-ensure-request-object-with-redirect-uri": {
271-
"browser": [
272-
{
273-
"comment": "expect an immediate error page",
274-
"match": "https://op.local.stack-dev.cirrusidentity.com/simplesaml/module.php/oidc/authorization*",
275-
"tasks": [
276-
{
277-
"task": "Expect redirect uri mismatch error page",
278-
"match": "https://op.local.stack-dev.cirrusidentity.com/simplesaml/module.php/oidc/authorization*",
279-
"commands": [
280-
[
281-
"wait",
282-
"xpath",
283-
"//*",
284-
10,
285-
"Check the `redirect_uri` parameter",
286-
"update-image-placeholder"
287-
]
288-
]
289-
}
290-
]
291-
}
292-
]
293-
},
294270
"oidcc-ensure-redirect-uri-in-authorization-request": {
295271
"browser": [
296272
{

conformance-tests/implicit-skips.json

-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,2 @@
11
[
2-
{
3-
"test-name": "oidcc-unsigned-request-object-supported-correctly-or-rejected-as-unsupported",
4-
"variant": "*",
5-
"configuration-filename": "*"
6-
}
72
]

docker/conformance.sql

+7-6
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,16 @@ CREATE TABLE oidc_client (
3535
backchannel_logout_uri TEXT NULL,
3636
entity_identifier VARCHAR(255) NULL,
3737
client_registration_types VARCHAR(255) NULL,
38-
federation_jwks TEXT NULL
38+
federation_jwks TEXT NULL,
39+
jwks TEXT NULL
3940
);
4041
-- Used 'httpd' host for back-channel logout url (https://httpd:8443/test/a/simplesamlphp-module-oidc/backchannel_logout)
4142
-- since this is the hostname of conformance server while running in container environment
42-
INSERT INTO oidc_client VALUES('_55a99a1d298da921cb27d700d4604352e51171ebc4','_8967dd97d07cc59db7055e84ac00e79005157c1132','Conformance Client 1',replace('Client 1 for Conformance Testing https://openid.net/certification/connect_op_testing/\n','\n',char(10)),'example-userpass','["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/callback","https:\/\/www.certification.openid.net\/test\/a\/simplesamlphp-module-oidc\/callback"]','["openid","profile","email","address","phone","offline_access"]',1,1,NULL,'["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/post_logout_redirect"]','https://httpd:8443/test/a/simplesamlphp-module-oidc/backchannel_logout',NULL,NULL, NULL);
43-
INSERT INTO oidc_client VALUES('_34efb61060172a11d62101bc804db789f8f9100b0e','_91a4607a1c10ba801268929b961b3f6c067ff82d21','Conformance Client 2','','example-userpass','["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/callback","https:\/\/www.certification.openid.net\/test\/a\/simplesamlphp-module-oidc\/callback"]','["openid","profile","email","offline_access"]',1,1,NULL,NULL,NULL,NULL,NULL, NULL);
44-
INSERT INTO oidc_client VALUES('_0afb7d18e54b2de8205a93e38ca119e62ee321d031','_944e73bbeec7850d32b68f1b5c780562c955967e4e','Conformance Client 3','Client for client_secret_post','example-userpass','["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/callback","https:\/\/www.certification.openid.net\/test\/a\/simplesamlphp-module-oidc\/callback"]','["openid","profile","email"]',1,1,NULL,NULL,NULL,NULL,NULL, NULL);
45-
INSERT INTO oidc_client VALUES('_8957eda35234902ba8343c0cdacac040310f17dfca','_322d16999f9da8b5abc9e9c0c08e853f60f4dc4804','RP-Initiated Logout Client','Client for testing RP-Initiated Logout','example-userpass','["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/callback","https:\/\/www.certification.openid.net\/test\/a\/simplesamlphp-module-oidc\/callback"]','["openid","profile","email","address","phone"]',1,1,NULL,'["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/post_logout_redirect"]',NULL,NULL,NULL, NULL);
46-
INSERT INTO oidc_client VALUES('_9fe2f7589ece1b71f5ef75a91847d71bc5125ec2a6','_3c0beb20194179c01d7796c6836f62801e9ed4b368','Back-Channel Logout Client','Client for testing Back-Channel Logout','example-userpass','["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/callback","https:\/\/www.certification.openid.net\/test\/a\/simplesamlphp-module-oidc\/callback"]','["openid","profile","email","address","phone"]',1,1,NULL,'["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/post_logout_redirect"]','https://httpd:8443/test/a/simplesamlphp-module-oidc/backchannel_logout',NULL,NULL, NULL);
43+
INSERT INTO oidc_client VALUES('_55a99a1d298da921cb27d700d4604352e51171ebc4','_8967dd97d07cc59db7055e84ac00e79005157c1132','Conformance Client 1',replace('Client 1 for Conformance Testing https://openid.net/certification/connect_op_testing/\n','\n',char(10)),'example-userpass','["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/callback","https:\/\/www.certification.openid.net\/test\/a\/simplesamlphp-module-oidc\/callback"]','["openid","profile","email","address","phone","offline_access"]',1,1,NULL,'["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/post_logout_redirect"]','https://httpd:8443/test/a/simplesamlphp-module-oidc/backchannel_logout',NULL,NULL, NULL, NULL);
44+
INSERT INTO oidc_client VALUES('_34efb61060172a11d62101bc804db789f8f9100b0e','_91a4607a1c10ba801268929b961b3f6c067ff82d21','Conformance Client 2','','example-userpass','["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/callback","https:\/\/www.certification.openid.net\/test\/a\/simplesamlphp-module-oidc\/callback"]','["openid","profile","email","offline_access"]',1,1,NULL,NULL,NULL,NULL,NULL, NULL, NULL);
45+
INSERT INTO oidc_client VALUES('_0afb7d18e54b2de8205a93e38ca119e62ee321d031','_944e73bbeec7850d32b68f1b5c780562c955967e4e','Conformance Client 3','Client for client_secret_post','example-userpass','["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/callback","https:\/\/www.certification.openid.net\/test\/a\/simplesamlphp-module-oidc\/callback"]','["openid","profile","email"]',1,1,NULL,NULL,NULL,NULL,NULL, NULL, NULL);
46+
INSERT INTO oidc_client VALUES('_8957eda35234902ba8343c0cdacac040310f17dfca','_322d16999f9da8b5abc9e9c0c08e853f60f4dc4804','RP-Initiated Logout Client','Client for testing RP-Initiated Logout','example-userpass','["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/callback","https:\/\/www.certification.openid.net\/test\/a\/simplesamlphp-module-oidc\/callback"]','["openid","profile","email","address","phone"]',1,1,NULL,'["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/post_logout_redirect"]',NULL,NULL,NULL, NULL, NULL);
47+
INSERT INTO oidc_client VALUES('_9fe2f7589ece1b71f5ef75a91847d71bc5125ec2a6','_3c0beb20194179c01d7796c6836f62801e9ed4b368','Back-Channel Logout Client','Client for testing Back-Channel Logout','example-userpass','["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/callback","https:\/\/www.certification.openid.net\/test\/a\/simplesamlphp-module-oidc\/callback"]','["openid","profile","email","address","phone"]',1,1,NULL,'["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/post_logout_redirect"]','https://httpd:8443/test/a/simplesamlphp-module-oidc/backchannel_logout',NULL,NULL, NULL, NULL);
4748
CREATE TABLE oidc_access_token (
4849
id VARCHAR(191) PRIMARY KEY NOT NULL,
4950
scopes TEXT,

phpunit.xml

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
bootstrap="./tests/bootstrap.php"
55
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd"
66
cacheDirectory="./build/phpunit-cache"
7+
displayDetailsOnTestsThatTriggerWarnings="true"
78
>
89
<coverage>
910
<report>

routing/services/services.yml

+9-3
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,19 @@ services:
7575
arguments:
7676
$publicKey: '@oidc.key.public'
7777

78-
SimpleSAML\Module\oidc\Utils\ClaimTranslatorExtractor:
79-
factory: ['@SimpleSAML\Module\oidc\Factories\ClaimTranslatorExtractorFactory', 'build']
80-
8178
SimpleSAML\Module\oidc\Server\AuthorizationServer:
8279
factory: ['@SimpleSAML\Module\oidc\Factories\AuthorizationServerFactory', 'build']
8380

8481
# OAuth2 Server
8582
League\OAuth2\Server\ResourceServer:
8683
factory: ['@SimpleSAML\Module\oidc\Factories\ResourceServerFactory', 'build']
8784

85+
# Utils
86+
SimpleSAML\Module\oidc\Utils\RequestParamsResolver: ~
87+
88+
SimpleSAML\Module\oidc\Utils\ClaimTranslatorExtractor:
89+
factory: ['@SimpleSAML\Module\oidc\Factories\ClaimTranslatorExtractorFactory', 'build']
90+
8891
# Use (already available) Laminas\Diactoros package as PSR HTTP Factories.
8992
Laminas\Diactoros\ServerRequestFactory: ~
9093
Laminas\Diactoros\RequestFactory: ~
@@ -94,3 +97,6 @@ services:
9497

9598
# Symfony
9699
Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory: ~
100+
101+
# Ssp OpenId
102+
SimpleSAML\OpenID\Core: ~

src/Controller/Client/CreateController.php

+3
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ public function __invoke(): Template|RedirectResponse
8787
$client['client_registration_types'] : null;
8888
/** @var ?array[] $federationJwks */
8989
$federationJwks = is_array($client['federation_jwks']) ? $client['federation_jwks'] : null;
90+
/** @var ?array[] $jwks */
91+
$jwks = is_array($client['jwks']) ? $client['jwks'] : null;
9092

9193
$this->clientRepository->add(ClientEntity::fromData(
9294
$client['id'],
@@ -104,6 +106,7 @@ public function __invoke(): Template|RedirectResponse
104106
empty($client['entity_identifier']) ? null : (string)$client['entity_identifier'],
105107
$clientRegistrationTypes,
106108
$federationJwks,
109+
$jwks,
107110
));
108111

109112
// Also persist allowed origins for this client.

src/Controller/Client/EditController.php

+3
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ public function __invoke(ServerRequest $request): Template|RedirectResponse
9494
$data['client_registration_types'] : null;
9595
/** @var ?array[] $federationJwks */
9696
$federationJwks = is_array($data['federation_jwks']) ? $data['federation_jwks'] : null;
97+
/** @var ?array[] $jwks */
98+
$jwks = is_array($data['jwks']) ? $data['jwks'] : null;
9799

98100
$this->clientRepository->update(ClientEntity::fromData(
99101
$client->getIdentifier(),
@@ -111,6 +113,7 @@ public function __invoke(ServerRequest $request): Template|RedirectResponse
111113
empty($data['entity_identifier']) ? null : (string)$data['entity_identifier'],
112114
$clientRegistrationTypes,
113115
$federationJwks,
116+
$jwks,
114117
), $authedUser);
115118

116119
// Also persist allowed origins for this client.

src/Controller/Federation/Test.php

+20-11
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@
44

55
namespace SimpleSAML\Module\oidc\Controller\Federation;
66

7-
use SimpleSAML\Module\oidc\ModuleConfig;
87
use SimpleSAML\Module\oidc\Services\LoggerService;
98
use SimpleSAML\OpenID\Codebooks\EntityTypeEnum;
9+
use SimpleSAML\OpenID\Core;
1010
use SimpleSAML\OpenID\Federation;
11-
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
12-
use Symfony\Component\Cache\Psr16Cache;
1311
use Symfony\Component\HttpFoundation\JsonResponse;
1412
use Symfony\Component\HttpFoundation\Response;
1513

@@ -20,18 +18,29 @@
2018
*/
2119
class Test
2220
{
23-
public function __construct(protected ModuleConfig $moduleConfig)
24-
{
25-
}
21+
// public function __construct(protected ModuleConfig $moduleConfig)
22+
// {
23+
// }
2624

2725
public function __invoke(): Response
2826
{
29-
$cache = new Psr16Cache(new FilesystemAdapter(
30-
'oidc-federation',
31-
60,
32-
$this->moduleConfig->sspConfig()->getPathValue('cachedir'),
33-
));
27+
// $cache = new Psr16Cache(new FilesystemAdapter(
28+
// 'oidc-federation',
29+
// 60,
30+
// $this->moduleConfig->sspConfig()->getPathValue('cachedir'),
31+
// ));
32+
33+
$requestObjectFactory = (new Core())->getRequestObjectFactory();
34+
35+
// {"alg":"none"}, {"iss":"joe",
36+
// "exp":1300819380,
37+
// "http://example.com/is_root":true}
38+
$unprotectedJws = 'eyJhbGciOiJub25lIn0.' .
39+
'eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.';
40+
41+
$requestObject = $requestObjectFactory->fromToken($unprotectedJws);
3442

43+
dd($requestObject, $requestObject->getPayload(), $requestObject->getHeader());
3544
// $cache->clear();
3645

3746
$trustChain = (new Federation(

src/Entities/ClientEntity.php

+22
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ class ClientEntity implements ClientEntityInterface
6060
* @var ?array[]|null
6161
*/
6262
private ?array $federationJwks = null;
63+
/**
64+
* @var ?array[]|null
65+
*/
66+
private ?array $jwks = null;
6367

6468
/**
6569
* Constructor.
@@ -74,6 +78,7 @@ private function __construct()
7478
* @param string[] $postLogoutRedirectUri
7579
* @param string[] $clientRegistrationTypes
7680
* @param array[] $federationJwks
81+
* @param array[] $jwks
7782
*/
7883
public static function fromData(
7984
string $id,
@@ -91,6 +96,7 @@ public static function fromData(
9196
?string $entityIdentifier = null,
9297
?array $clientRegistrationTypes = null,
9398
?array $federationJwks = null,
99+
?array $jwks = null,
94100
): ClientEntityInterface {
95101
$client = new self();
96102

@@ -109,6 +115,7 @@ public static function fromData(
109115
$client->entityIdentifier = empty($entityIdentifier) ? null : $entityIdentifier;
110116
$client->clientRegistrationTypes = $clientRegistrationTypes;
111117
$client->federationJwks = $federationJwks;
118+
$client->jwks = $jwks;
112119

113120
return $client;
114121
}
@@ -179,6 +186,12 @@ public static function fromState(array $state): self
179186
json_decode((string)$state['federation_jwks'], true, 512, JSON_THROW_ON_ERROR);
180187
$client->federationJwks = $federationJwks;
181188

189+
/** @var ?array[] $jwks */
190+
$jwks = empty($state['jwks']) ?
191+
null :
192+
json_decode((string)$state['jwks'], true, 512, JSON_THROW_ON_ERROR);
193+
$client->jwks = $jwks;
194+
182195
return $client;
183196
}
184197

@@ -208,6 +221,9 @@ public function getState(): array
208221
'federation_jwks' => is_null($this->federationJwks) ?
209222
null :
210223
json_encode($this->getFederationJwks()),
224+
'jwks' => is_null($this->jwks) ?
225+
null :
226+
json_encode($this->jwks()),
211227
];
212228
}
213229

@@ -229,6 +245,7 @@ public function toArray(): array
229245
'entity_identifier' => $this->entityIdentifier,
230246
'client_registration_types' => $this->clientRegistrationTypes,
231247
'federation_jwks' => $this->federationJwks,
248+
'jwks' => $this->jwks,
232249
];
233250
}
234251

@@ -322,4 +339,9 @@ public function getFederationJwks(): ?array
322339
{
323340
return $this->federationJwks;
324341
}
342+
343+
public function jwks(): ?array
344+
{
345+
return $this->jwks;
346+
}
325347
}

src/Entities/Interfaces/ClientEntityInterface.php

+5
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,9 @@ public function getClientRegistrationTypes(): array;
8080
* @return array[]|null
8181
*/
8282
public function getFederationJwks(): ?array;
83+
84+
/**
85+
* @return array[]|null
86+
*/
87+
public function jwks(): ?array;
8388
}

src/Factories/Grant/AuthCodeGrantFactory.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616

1717
namespace SimpleSAML\Module\oidc\Factories\Grant;
1818

19-
use SimpleSAML\Module\oidc\Helpers;
2019
use SimpleSAML\Module\oidc\ModuleConfig;
2120
use SimpleSAML\Module\oidc\Repositories\AccessTokenRepository;
2221
use SimpleSAML\Module\oidc\Repositories\AuthCodeRepository;
2322
use SimpleSAML\Module\oidc\Repositories\RefreshTokenRepository;
2423
use SimpleSAML\Module\oidc\Server\Grants\AuthCodeGrant;
2524
use SimpleSAML\Module\oidc\Server\RequestRules\RequestRulesManager;
25+
use SimpleSAML\Module\oidc\Utils\RequestParamsResolver;
2626

2727
class AuthCodeGrantFactory
2828
{
@@ -32,7 +32,7 @@ public function __construct(
3232
private readonly AccessTokenRepository $accessTokenRepository,
3333
private readonly RefreshTokenRepository $refreshTokenRepository,
3434
private readonly RequestRulesManager $requestRulesManager,
35-
private readonly Helpers $helpers,
35+
private readonly RequestParamsResolver $requestParamsResolver,
3636
) {
3737
}
3838

@@ -47,7 +47,7 @@ public function build(): AuthCodeGrant
4747
$this->refreshTokenRepository,
4848
$this->moduleConfig->getAuthCodeDuration(),
4949
$this->requestRulesManager,
50-
$this->helpers,
50+
$this->requestParamsResolver,
5151
);
5252
$authCodeGrant->setRefreshTokenTTL($this->moduleConfig->getRefreshTokenDuration());
5353

src/Factories/Grant/ImplicitGrantFactory.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
*/
1616
namespace SimpleSAML\Module\oidc\Factories\Grant;
1717

18-
use SimpleSAML\Module\oidc\Helpers;
1918
use SimpleSAML\Module\oidc\ModuleConfig;
2019
use SimpleSAML\Module\oidc\Repositories\AccessTokenRepository;
2120
use SimpleSAML\Module\oidc\Server\Grants\ImplicitGrant;
2221
use SimpleSAML\Module\oidc\Server\RequestRules\RequestRulesManager;
2322
use SimpleSAML\Module\oidc\Services\IdTokenBuilder;
23+
use SimpleSAML\Module\oidc\Utils\RequestParamsResolver;
2424

2525
class ImplicitGrantFactory
2626
{
@@ -29,7 +29,7 @@ public function __construct(
2929
private readonly IdTokenBuilder $idTokenBuilder,
3030
private readonly RequestRulesManager $requestRulesManager,
3131
private readonly AccessTokenRepository $accessTokenRepository,
32-
private readonly Helpers $helpers,
32+
private readonly RequestParamsResolver $requestParamsResolver,
3333
) {
3434
}
3535

@@ -39,9 +39,9 @@ public function build(): ImplicitGrant
3939
$this->idTokenBuilder,
4040
$this->moduleConfig->getAccessTokenDuration(),
4141
$this->accessTokenRepository,
42-
'#',
4342
$this->requestRulesManager,
44-
$this->helpers,
43+
$this->requestParamsResolver,
44+
'#',
4545
);
4646
}
4747
}

0 commit comments

Comments
 (0)