LoginHandlerTest::testSkipRegistration()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 35
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 18
c 1
b 0
f 0
nc 4
nop 3
dl 0
loc 35
rs 9.6666
1
<?php
2
3
namespace SilverStripe\MFA\Tests\Authenticator;
4
5
use PHPUnit_Framework_MockObject_MockObject;
6
use SilverStripe\Control\Controller;
7
use SilverStripe\Control\HTTPRequest;
8
use SilverStripe\Control\HTTPResponse;
9
use SilverStripe\Control\Session;
10
use SilverStripe\Core\Config\Config;
11
use SilverStripe\Core\Injector\Injector;
12
use SilverStripe\Dev\FunctionalTest;
13
use SilverStripe\MFA\Authenticator\LoginHandler;
14
use SilverStripe\MFA\Authenticator\MemberAuthenticator;
15
use SilverStripe\MFA\Extension\MemberExtension;
16
use SilverStripe\MFA\Method\Handler\VerifyHandlerInterface;
17
use SilverStripe\MFA\Method\MethodInterface;
18
use SilverStripe\MFA\Model\RegisteredMethod;
19
use SilverStripe\MFA\Service\MethodRegistry;
20
use SilverStripe\MFA\Service\RegisteredMethodManager;
21
use SilverStripe\MFA\State\Result;
22
use SilverStripe\MFA\Store\SessionStore;
23
use SilverStripe\MFA\Store\StoreInterface;
24
use SilverStripe\MFA\Tests\Stub\BasicMath\Method;
25
use SilverStripe\ORM\FieldType\DBDatetime;
26
use SilverStripe\Security\Member;
27
use SilverStripe\Security\Security;
28
use SilverStripe\Security\SecurityToken;
29
use SilverStripe\SecurityExtensions\Service\SudoModeServiceInterface;
30
use SilverStripe\SiteConfig\SiteConfig;
31
32
class LoginHandlerTest extends FunctionalTest
33
{
34
    protected static $fixture_file = 'LoginHandlerTest.yml';
35
36
    protected function setUp()
37
    {
38
        parent::setUp();
39
        Config::modify()->set(MethodRegistry::class, 'methods', [Method::class]);
40
41
        Injector::inst()->load([
42
            Security::class => [
43
                'properties' => [
44
                    'authenticators' => [
45
                        'default' => '%$' . MemberAuthenticator::class,
46
                    ]
47
                ]
48
            ]
49
        ]);
50
51
        /** @var SudoModeServiceInterface&PHPUnit_Framework_MockObject_MockObject $sudoModeService */
52
        $sudoModeService = $this->createMock(SudoModeServiceInterface::class);
53
        $sudoModeService->expects($this->any())->method('check')->willReturn(true);
54
        Injector::inst()->registerService($sudoModeService, SudoModeServiceInterface::class);
55
    }
56
57
    public function testMFAStepIsAdded()
58
    {
59
        /** @var Member&MemberExtension $member */
60
        $member = $this->objFromFixture(Member::class, 'guy');
61
62
        $this->autoFollowRedirection = false;
63
        $response = $this->doLogin($member, 'Password123');
64
        $this->autoFollowRedirection = true;
65
66
        $this->assertSame(302, $response->getStatusCode());
67
        $this->assertStringEndsWith(
68
            Controller::join_links(Security::login_url(), 'default/mfa'),
69
            $response->getHeader('location')
70
        );
71
    }
72
73
    public function testMFARedirectsBackWhenSudoModeIsInactive()
74
    {
75
        /** @var SudoModeServiceInterface&PHPUnit_Framework_MockObject_MockObject $sudoModeService */
76
        $sudoModeService = $this->createMock(SudoModeServiceInterface::class);
77
        $sudoModeService->expects($this->once())->method('check')->willReturn(false);
78
        Injector::inst()->registerService($sudoModeService, SudoModeServiceInterface::class);
79
80
        /** @var Member&MemberExtension $member */
81
        $member = $this->objFromFixture(Member::class, 'guy');
82
        $this->scaffoldPartialLogin($member);
83
84
        $this->autoFollowRedirection = false;
85
        $response = $this->get(Controller::join_links(Security::login_url(), 'default/mfa'));
86
87
        $this->assertSame(302, $response->getStatusCode());
88
    }
89
90
    public function testMethodsNotBeingAvailableWillLogin()
91
    {
92
        Config::modify()->set(MethodRegistry::class, 'methods', []);
93
94
        /** @var Member&MemberExtension $member */
95
        $member = $this->objFromFixture(Member::class, 'guy');
96
97
        // Ensure a URL is set to redirect to after successful login
98
        $this->session()->set('BackURL', 'something');
99
100
        $this->autoFollowRedirection = false;
101
        $response = $this->doLogin($member, 'Password123');
102
        $this->autoFollowRedirection = true;
103
104
        $this->assertSame(302, $response->getStatusCode());
105
        $this->assertStringEndsWith('/something', $response->getHeader('location'));
106
    }
107
108
    public function testMFASchemaEndpointIsNotAccessibleByDefault()
109
    {
110
        // Assert that this endpoint is not available if you haven't started the login process
111
        $this->autoFollowRedirection = false;
112
        $response = $this->get(Controller::join_links(Security::login_url(), 'default/mfa/schema'));
113
        $this->autoFollowRedirection = true;
114
115
        $this->assertSame(302, $response->getStatusCode());
116
    }
117
118
    public function testMFASchemaEndpointReturnsMethodDetails()
119
    {
120
        // "Guy" isn't very security conscious - he has no MFA methods set up
121
        /** @var Member&MemberExtension $member */
122
        $member = $this->objFromFixture(Member::class, 'guy');
123
        $this->scaffoldPartialLogin($member);
124
125
        $result = $this->get(Controller::join_links(Security::login_url(), 'default/mfa/schema'));
126
127
        $response = json_decode($result->getBody(), true);
128
129
        $this->assertArrayHasKey('registeredMethods', $response);
130
        $this->assertArrayHasKey('availableMethods', $response);
131
        $this->assertArrayHasKey('defaultMethod', $response);
132
133
        $this->assertCount(0, $response['registeredMethods']);
134
        $this->assertCount(1, $response['availableMethods']);
135
        $this->assertNull($response['defaultMethod']);
136
137
        /** @var MethodInterface $method */
138
        $method = Injector::inst()->get(Method::class);
139
        $registerHandler = $method->getRegisterHandler();
140
141
        $methods = $response['availableMethods'];
142
        $this->assertNotEmpty($methods);
143
        $firstMethod = $methods[0];
144
145
        $this->assertSame($method->getURLSegment(), $firstMethod['urlSegment']);
146
        $this->assertSame($method->getName(), $firstMethod['name']);
147
        $this->assertSame($registerHandler->getDescription(), $firstMethod['description']);
148
        $this->assertSame($registerHandler->getSupportLink(), $firstMethod['supportLink']);
149
        $this->assertContains('client/dist/images', $firstMethod['thumbnail']);
150
        $this->assertSame('BasicMathRegister', $firstMethod['component']);
151
    }
152
153
    public function testMFASchemaEndpointShowsRegisteredMethodsIfSetUp()
154
    {
155
        // "Simon" is security conscious - he uses the cutting edge MFA methods
156
        /** @var Member&MemberExtension $member */
157
        $member = $this->objFromFixture(Member::class, 'simon');
158
        $this->scaffoldPartialLogin($member);
159
160
        $result = $this->get(Controller::join_links(Security::login_url(), 'default/mfa/schema'));
161
162
        $response = json_decode($result->getBody(), true);
163
164
        $this->assertArrayHasKey('registeredMethods', $response);
165
        $this->assertArrayHasKey('availableMethods', $response);
166
        $this->assertArrayHasKey('defaultMethod', $response);
167
168
        $this->assertCount(1, $response['registeredMethods']);
169
        $this->assertCount(0, $response['availableMethods']);
170
        $this->assertNull($response['defaultMethod']);
171
172
        /** @var MethodInterface $method */
173
        $method = Injector::inst()->get(Method::class);
174
175
        $result = $response['registeredMethods'][0];
176
        $this->assertSame($method->getURLSegment(), $result['urlSegment']);
177
        $this->assertSame($method->getName(), $result['name']);
178
        $this->assertSame('BasicMathLogin', $result['component']);
179
        $this->assertSame('https://google.com', $result['supportLink']);
180
        $this->assertContains('totp.svg', $result['thumbnail']);
181
    }
182
183
    public function testMFASchemaEndpointProvidesDefaultMethodIfSet()
184
    {
185
        // "Robbie" is security conscious and is also a CMS expert! He set up MFA and set a default method :o
186
        /** @var Member&MemberExtension $member */
187
        $member = $this->objFromFixture(Member::class, 'robbie');
188
        $this->scaffoldPartialLogin($member);
189
190
        $result = $this->get(Controller::join_links(Security::login_url(), 'default/mfa/schema'));
191
192
        $response = json_decode($result->getBody(), true);
193
194
        $this->assertArrayHasKey('registeredMethods', $response);
195
        $this->assertArrayHasKey('availableMethods', $response);
196
        $this->assertArrayHasKey('defaultMethod', $response);
197
198
        $this->assertCount(1, $response['registeredMethods']);
199
        $this->assertCount(0, $response['availableMethods']);
200
201
        /** @var RegisteredMethod $mathMethod */
202
        $mathMethod = $this->objFromFixture(RegisteredMethod::class, 'robbie-math');
203
        $this->assertSame($mathMethod->getMethod()->getURLSegment(), $response['defaultMethod']);
204
    }
205
206
    /**
207
     * @param bool $mfaRequired
208
     * @param string|null $member
209
     * @dataProvider cannotSkipMFAProvider
210
     */
211
    public function testCannotSkipMFA($mfaRequired, $member = 'robbie')
212
    {
213
        $this->setSiteConfig(['MFARequired' => $mfaRequired]);
214
215
        if ($member) {
216
            $this->scaffoldPartialLogin($this->objFromFixture(Member::class, $member));
217
        }
218
219
        $response = $this->get(Controller::join_links(Security::login_url(), 'default/mfa/skip'));
220
        $this->assertContains('You cannot skip MFA registration', $response->getBody());
221
    }
222
223
    /**
224
     * @return array[]
225
     */
226
    public function cannotSkipMFAProvider()
227
    {
228
        return [
229
            'mfa is required' => [true],
230
            'mfa is not required, but user already has configured methods' => [false],
231
            'no member is available' => [false, null],
232
        ];
233
    }
234
235
    /**
236
     * @param string $memberFixture
237
     * @param bool $mfaRequiredInGrace
238
     * @param string|null $expectedRedirect
239
     * @dataProvider skipRegistrationProvider
240
     */
241
    public function testSkipRegistration($memberFixture, $mfaRequiredInGrace = false, $expectedRedirect = null)
242
    {
243
        if ($mfaRequiredInGrace) {
244
            $this->setSiteConfig([
245
                'MFARequired' => true,
246
                'MFAGracePeriodExpires' => DBDatetime::now()
247
                    ->setValue(strtotime('+1 day', DBDatetime::now()->getTimestamp()))->Rfc2822()
248
            ]);
249
        } else {
250
            $this->setSiteConfig(['MFARequired' => false]);
251
        }
252
253
        if (!$expectedRedirect) {
254
            $expectedRedirect = Controller::join_links(Security::login_url(), 'default');
255
        }
256
257
        /** @var Member $member */
258
        $member = $this->objFromFixture(Member::class, $memberFixture);
259
        $this->scaffoldPartialLogin($member);
260
261
        $this->autoFollowRedirection = false;
262
        $response = $this->get(Controller::join_links(Security::login_url(), 'default/mfa/skip'));
263
264
        // Assert a redirect is given
265
        $this->assertSame(302, $response->getStatusCode());
266
267
        // Assert the redirect is to the expected location
268
        $this->assertStringEndsWith($expectedRedirect, $response->getHeader('location'));
269
270
        // Assert the user is now logged in
271
        $this->assertSame($member->ID, Security::getCurrentUser()->ID, 'User is successfully logged in');
272
273
        // Assert that the member is tracked as having skipped registration
274
        $member = Member::get()->byID($member->ID);
275
        $this->assertTrue((bool)$member->HasSkippedMFARegistration);
276
    }
277
278
    public function skipRegistrationProvider()
279
    {
280
        return [
281
            ['guy'],
282
            ['guy', true],
283
            ['pete', false, 'Security/changepassword'],
284
            ['pete', true, 'Security/changepassword'],
285
        ];
286
    }
287
288
    /**
289
     * @param string $memberFixture
290
     * @dataProvider methodlessMemberFixtureProvider
291
     */
292
    public function testBackURLIsPreservedWhenSkipping($memberFixture)
293
    {
294
        /** @var Member $member */
295
        $member = $this->objFromFixture(Member::class, $memberFixture);
296
        $this->scaffoldPartialLogin($member);
297
298
        $this->doLogin($member, 'Password123', 'admin/pages');
299
300
        $this->autoFollowRedirection = false;
301
        $response = $this->get(Controller::join_links(Security::login_url(), 'default/mfa/skip'));
302
303
        $this->assertSame(302, $response->getStatusCode());
304
        $this->assertStringEndsWith('admin/pages', $response->getHeader('location'));
305
    }
306
307
    /**
308
     * @expectedException \SilverStripe\MFA\Exception\MemberNotFoundException
309
     */
310
    public function testGetMemberThrowsExceptionWithoutMember()
311
    {
312
        $this->logOut();
313
        $handler = new LoginHandler('foo', $this->createMock(MemberAuthenticator::class));
314
        $handler->setRequest(new HTTPRequest('GET', '/'));
315
        $handler->getRequest()->setSession(new Session([]));
316
        $handler->getMember();
317
    }
318
319
    public function testStartVerificationIncludesACSRFToken()
320
    {
321
        SecurityToken::enable();
322
323
        $handler = new LoginHandler('mfa', $this->createMock(MemberAuthenticator::class));
324
        $member = $this->objFromFixture(Member::class, 'robbie');
325
        $store = new SessionStore($member);
326
        $handler->setStore($store);
327
328
        $request = new HTTPRequest('GET', '/');
329
        $request->setSession(new Session([]));
330
        $request->setRouteParams(['Method' => 'basic-math']);
331
        $response = json_decode($handler->startVerification($request)->getBody());
332
333
        $this->assertNotNull($response->SecurityID);
334
        $this->assertTrue(SecurityToken::inst()->check($response->SecurityID));
335
    }
336
337
    public function testVerifyAssertsValidCSRFToken()
338
    {
339
        SecurityToken::enable();
340
341
        $handler = new LoginHandler('mfa', $this->createMock(MemberAuthenticator::class));
342
        $member = $this->objFromFixture(Member::class, 'robbie');
343
        $store = new SessionStore($member);
344
        $store->setMethod('basic-math');
345
        $handler->setStore($store);
346
347
        $request = new HTTPRequest('GET', '/');
348
        $request->setSession(new Session([]));
349
350
        $response = $handler->finishVerification($request);
351
352
        $this->assertSame(403, $response->getStatusCode());
353
        $this->assertContains('Your request timed out', $response->getBody());
354
355
        $request = new HTTPRequest('GET', '/', [
356
            SecurityToken::inst()->getName() => SecurityToken::inst()->getValue()
357
        ]);
358
        $request->setSession(new Session([]));
359
360
        // Mock the verification process...
361
        $mockVerifyHandler = $this->createMock(VerifyHandlerInterface::class);
362
        $mockRegisteredMethod = $this->createMock(RegisteredMethod::class);
363
        $mockRegisteredMethodManager = $this->createMock(RegisteredMethodManager::class);
364
365
        $mockRegisteredMethodManager
366
            ->expects($this->once())->method('getFromMember')->willReturn($mockRegisteredMethod);
367
        $mockRegisteredMethod->expects($this->once())->method('getVerifyHandler')->willReturn($mockVerifyHandler);
368
        $mockVerifyHandler->expects($this->once())->method('verify')->willReturn(Result::create());
369
370
        // Register our mock service
371
        Injector::inst()->registerService($mockRegisteredMethodManager, RegisteredMethodManager::class);
372
373
        $response = $handler->finishVerification($request);
374
375
        $this->assertSame(200, $response->getStatusCode());
376
    }
377
378
    public function testStartVerificationReturnsForbiddenWithoutMember()
379
    {
380
        $this->logOut();
381
382
        $handler = new LoginHandler('mfa', $this->createMock(MemberAuthenticator::class));
383
        $handler->setRequest(new HTTPRequest('GET', '/'));
384
        $handler->getRequest()->setSession(new Session([]));
385
        $handler->setStore($this->createMock(StoreInterface::class));
386
387
        $response = $handler->startVerification($handler->getRequest());
388
        $this->assertSame(403, $response->getStatusCode());
389
    }
390
391
    public function testStartVerificationReturnsForbiddenWithoutSudoMode()
392
    {
393
        /** @var Member&MemberExtension $member */
394
        $member = $this->objFromFixture(Member::class, 'robbie');
395
        $this->scaffoldPartialLogin($member);
396
397
        /** @var SudoModeServiceInterface&PHPUnit_Framework_MockObject_MockObject $sudoModeService */
398
        $sudoModeService = $this->createMock(SudoModeServiceInterface::class);
399
        $sudoModeService->method('check')->willReturn(false);
400
        Injector::inst()->registerService($sudoModeService, SudoModeServiceInterface::class);
401
402
        $handler = new LoginHandler('mfa', $this->createMock(MemberAuthenticator::class));
403
        $handler->setRequest(new HTTPRequest('GET', '/'));
404
        $handler->getRequest()->setSession(new Session([]));
405
406
        $store = new SessionStore($member);
407
        $store->setMethod('basic-math');
408
        $handler->setStore($store);
409
410
        $response = $handler->startVerification($handler->getRequest());
411
        $this->assertSame(403, $response->getStatusCode());
412
    }
413
414
    public function testFinishVerificationHandlesMembersLockedOut()
415
    {
416
        /** @var Member&MemberExtension $member */
417
        $member = $this->objFromFixture(Member::class, 'robbie');
418
        // Mock the member being locked out for fifteen minutes
419
        $member->LockedOutUntil = date('Y-m-d H:i:s', DBDatetime::now()->getTimestamp() + 15 * 60);
420
        $member->write();
421
422
        $handler = new LoginHandler('mfa', $this->createMock(MemberAuthenticator::class));
423
        $request = new HTTPRequest('GET', '/');
424
        $request->setSession(new Session([]));
425
426
        $store = new SessionStore($member);
427
        $store->setMethod('basic-math');
428
        $handler->setStore($store);
429
430
        $response = $handler->finishVerification($request);
431
        $this->assertEquals(403, $response->getStatusCode());
432
        $this->assertContains('Your account is temporarily locked', (string) $response->getBody());
433
    }
434
435
    public function testFinishVerificationChecksSudoModeIsActive()
436
    {
437
        /** @var Member&MemberExtension $member */
438
        $member = $this->objFromFixture(Member::class, 'robbie');
439
440
        $handler = new LoginHandler('mfa', $this->createMock(MemberAuthenticator::class));
441
        $request = new HTTPRequest('GET', '/');
442
        $request->setSession(new Session([]));
443
444
        $store = new SessionStore($member);
445
        $store->setMethod('basic-math');
446
        $handler->setStore($store);
447
448
        /** @var SudoModeServiceInterface&PHPUnit_Framework_MockObject_MockObject $sudoModeService */
449
        $sudoModeService = $this->createMock(SudoModeServiceInterface::class);
450
        $sudoModeService->method('check')->willReturn(false);
451
        Injector::inst()->registerService($sudoModeService, SudoModeServiceInterface::class);
452
453
        $response = $handler->finishVerification($request);
454
        $this->assertEquals(403, $response->getStatusCode());
455
        $this->assertContains('You need to re-verify your account before continuing', (string) $response->getBody());
456
    }
457
458
    public function testFinishVerificationPassesExceptionMessagesThroughFromMethodsWithValidationFailures()
459
    {
460
        /** @var Member&MemberExtension $member */
461
        $member = $this->objFromFixture(Member::class, 'robbie');
462
        $member->config()->set('lock_out_after_incorrect_logins', 5);
463
        $failedLogins = $member->FailedLoginCount;
464
465
        /** @var LoginHandler|PHPUnit_Framework_MockObject_MockObject $handler */
466
        $handler = $this->getMockBuilder(LoginHandler::class)
467
            ->setMethods(['completeVerificationRequest'])
468
            ->disableOriginalConstructor()
469
            ->getMock();
470
471
        $handler->expects($this->once())->method('completeVerificationRequest')->willReturn(
472
            Result::create(false, 'It failed because it\'s mocked, obviously')
473
        );
474
475
        $request = new HTTPRequest('GET', '/');
476
        $request->setSession(new Session([]));
477
        $store = new SessionStore($member);
478
        $store->setMethod('basic-math');
479
        $handler->setStore($store);
480
481
        $response = $handler->finishVerification($request);
482
483
        $this->assertEquals(401, $response->getStatusCode());
484
        $this->assertContains('It failed because it\'s mocked', (string) $response->getBody());
485
        $this->assertSame($failedLogins + 1, $member->FailedLoginCount, 'Failed login is registered');
486
    }
487
488
    /**
489
     * @param string $memberFixture
490
     * @dataProvider methodlessMemberFixtureProvider
491
     */
492
    public function testFinishVerificationWillRedirectToTheBackURLSetAsLoginIsStarted($memberFixture)
493
    {
494
        /** @var Member $member */
495
        $member = $this->objFromFixture(Member::class, $memberFixture);
496
        $this->scaffoldPartialLogin($member);
497
498
        $this->doLogin($member, 'Password123', 'admin/pages');
499
500
        /** @var LoginHandler|PHPUnit_Framework_MockObject_MockObject $handler */
501
        $handler = $this->getMockBuilder(LoginHandler::class)
502
            ->setMethods(['completeVerificationRequest'])
503
            ->disableOriginalConstructor()
504
            ->getMock();
505
506
        $handler->expects($this->once())->method('completeVerificationRequest')->willReturn(Result::create());
507
508
        $request = new HTTPRequest('GET', '/');
509
        $request->setSession(new Session([]));
510
        $store = new SessionStore($member);
511
        $store->setMethod('basic-math');
512
        $handler->setStore($store);
513
514
        $response = $handler->finishVerification($request);
515
516
        // Assert "Accepted" response
517
        $this->assertSame(202, $response->getStatusCode());
518
519
        $this->autoFollowRedirection = false;
520
        $response = $this->get(Controller::join_links(Security::login_url(), 'default/mfa/complete'));
521
        $this->assertSame(302, $response->getStatusCode());
522
        $this->assertStringEndsWith('admin/pages', $response->getHeader('location'));
523
    }
524
525
    public function testGetBackURL()
526
    {
527
        $handler = new LoginHandler('foo', $this->createMock(MemberAuthenticator::class));
528
529
        $request = new HTTPRequest('GET', '/');
530
        $handler->setRequest($request);
531
532
        $session = new Session([]);
533
        $request->setSession($session);
534
535
        $session->set(LoginHandler::SESSION_KEY . '.additionalData', ['BackURL' => 'foobar']);
536
537
        $this->assertSame('foobar', $handler->getBackURL());
538
    }
539
540
    public function methodlessMemberFixtureProvider()
541
    {
542
        return [['guy', 'carla']];
543
    }
544
545
    /**
546
     * Mark the given user as partially logged in - ie. they've entered their email/password and are currently going
547
     * through the MFA process
548
     * @param Member $member
549
     */
550
    protected function scaffoldPartialLogin(Member $member)
551
    {
552
        $this->logOut();
553
554
        $this->session()->set(SessionStore::SESSION_KEY, new SessionStore($member));
555
    }
556
557
    /**
558
     * @param Member $member
559
     * @param string $password
560
     * @return HTTPResponse
561
     */
562
    protected function doLogin(Member $member, $password, $backUrl = null)
563
    {
564
        $url = Config::inst()->get(Security::class, 'login_url');
565
566
        if ($backUrl) {
567
            $url .= '?BackURL=' . $backUrl;
568
        }
569
570
        $this->get($url);
571
572
        return $this->submitForm(
573
            'MemberLoginForm_LoginForm',
574
            null,
575
            [
576
                'Email' => $member->Email,
577
                'Password' => $password,
578
                'AuthenticationMethod' => MemberAuthenticator::class,
579
                'action_doLogin' => 1,
580
            ]
581
        );
582
    }
583
584
    /**
585
     * Helper method for changing the current SiteConfig values
586
     *
587
     * @param array $data
588
     */
589
    protected function setSiteConfig(array $data)
590
    {
591
        $siteConfig = SiteConfig::current_site_config();
592
        $siteConfig->update($data);
593
        $siteConfig->write();
594
    }
595
}
596