2018-03-22 18:28:36 +01:00
|
|
|
# Default 2FA engine
|
|
|
|
#
|
|
|
|
# 2FA engine provides 3 functions and 1 interface:
|
|
|
|
# - init()
|
|
|
|
# - run($req): called during auth process after session populating
|
2018-03-26 10:33:04 +02:00
|
|
|
# - display2fRegisters($req, $session): indicates if a 2F registration is
|
2018-03-22 18:28:36 +01:00
|
|
|
# available for this user
|
|
|
|
# - /2fregisters: the URL path that displays 2F registration menu
|
|
|
|
|
2018-03-08 16:33:34 +01:00
|
|
|
package Lemonldap::NG::Portal::2F::Engines::Default;
|
|
|
|
|
|
|
|
use strict;
|
|
|
|
use Mouse;
|
2018-03-31 00:16:36 +02:00
|
|
|
use JSON qw(from_json to_json);
|
2018-03-08 16:33:34 +01:00
|
|
|
use Lemonldap::NG::Portal::Main::Constants qw(
|
2018-10-12 10:04:03 +02:00
|
|
|
PE_ERROR
|
|
|
|
PE_NOTOKEN
|
|
|
|
PE_OK
|
|
|
|
PE_SENDRESPONSE
|
|
|
|
PE_TOKENEXPIRED
|
2018-03-08 16:33:34 +01:00
|
|
|
);
|
|
|
|
|
2018-12-18 13:11:50 +01:00
|
|
|
our $VERSION = '2.0.1';
|
2018-03-08 16:33:34 +01:00
|
|
|
|
|
|
|
extends 'Lemonldap::NG::Portal::Main::Plugin';
|
|
|
|
|
2018-03-08 18:51:01 +01:00
|
|
|
# INITIALIZATION
|
|
|
|
|
|
|
|
has sfModules => ( is => 'rw', default => sub { [] } );
|
|
|
|
|
|
|
|
has sfRModules => ( is => 'rw', default => sub { [] } );
|
|
|
|
|
2018-08-15 20:42:08 +02:00
|
|
|
has sfReq => ( is => 'rw' );
|
|
|
|
|
2018-03-08 20:36:32 +01:00
|
|
|
has ott => (
|
|
|
|
is => 'rw',
|
2018-11-16 17:30:57 +01:00
|
|
|
lazy => 1,
|
2018-03-08 20:36:32 +01:00
|
|
|
default => sub {
|
2018-10-12 10:04:03 +02:00
|
|
|
my $ott =
|
|
|
|
$_[0]->{p}->loadModule('Lemonldap::NG::Portal::Lib::OneTimeToken');
|
2018-03-08 20:36:32 +01:00
|
|
|
$ott->timeout( $_[0]->{conf}->{formTimeout} );
|
|
|
|
return $ott;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2018-03-08 16:33:34 +01:00
|
|
|
sub init {
|
2018-03-08 18:51:01 +01:00
|
|
|
my ($self) = @_;
|
|
|
|
|
|
|
|
# Load 2F modules
|
|
|
|
for my $i ( 0 .. 1 ) {
|
|
|
|
foreach (
|
2018-03-08 20:36:32 +01:00
|
|
|
split /,\s*/,
|
2018-10-12 19:40:13 +02:00
|
|
|
$self->conf->{
|
|
|
|
$i
|
2018-09-12 23:14:35 +02:00
|
|
|
? 'available2FSelfRegistration'
|
2018-10-12 19:40:13 +02:00
|
|
|
: 'available2F'
|
|
|
|
}
|
2018-10-12 10:04:03 +02:00
|
|
|
)
|
2018-03-08 18:51:01 +01:00
|
|
|
{
|
|
|
|
my $prefix = lc($_);
|
|
|
|
$prefix =~ s/2f$//i;
|
|
|
|
|
|
|
|
# Activation parameter
|
2018-03-08 20:36:32 +01:00
|
|
|
my $ap = $prefix . ( $i ? '2fSelfRegistration' : '2fActivation' );
|
|
|
|
$self->logger->debug("Checking $ap");
|
2018-03-08 18:51:01 +01:00
|
|
|
|
|
|
|
# Unless $rule, skip loading
|
2018-03-08 20:36:32 +01:00
|
|
|
if ( $self->conf->{$ap} ) {
|
2018-03-08 18:51:01 +01:00
|
|
|
$self->logger->debug("Trying to load $_ 2F");
|
2018-10-12 10:04:03 +02:00
|
|
|
my $m =
|
|
|
|
$self->p->loadPlugin( $i ? "::2F::Register::$_" : "::2F::$_" )
|
|
|
|
or return 0;
|
2018-03-08 18:51:01 +01:00
|
|
|
|
|
|
|
# Rule and prefix may be modified by 2F module, reread them
|
|
|
|
my $rule = $self->conf->{$ap};
|
|
|
|
$prefix = $m->prefix;
|
|
|
|
|
|
|
|
# Compile rule
|
|
|
|
$rule = $self->p->HANDLER->substitute($rule);
|
|
|
|
unless ( $rule = $self->p->HANDLER->buildSub($rule) ) {
|
|
|
|
$self->error( 'External 2F rule error: '
|
2018-10-12 10:04:03 +02:00
|
|
|
. $self->p->HANDLER->tsv->{jail}->error );
|
2018-03-08 18:51:01 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Store module
|
|
|
|
push @{ $self->{ $i ? 'sfRModules' : 'sfModules' } },
|
2018-10-12 10:04:03 +02:00
|
|
|
{ p => $prefix, m => $m, r => $rule };
|
2018-03-08 18:51:01 +01:00
|
|
|
}
|
2018-03-08 20:36:32 +01:00
|
|
|
else {
|
|
|
|
$self->logger->debug(' -> not enabled');
|
|
|
|
}
|
2018-03-08 18:51:01 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-15 20:42:08 +02:00
|
|
|
unless (
|
|
|
|
$self->sfReq(
|
|
|
|
$self->p->HANDLER->buildSub(
|
|
|
|
$self->p->HANDLER->substitute( $self->conf->{sfRequired} )
|
|
|
|
)
|
|
|
|
)
|
2018-10-12 10:04:03 +02:00
|
|
|
)
|
2018-08-15 20:42:08 +02:00
|
|
|
{
|
|
|
|
$self->error( 'Error in sfRequired rule'
|
2018-10-12 10:04:03 +02:00
|
|
|
. $self->p->HANDLER->tsv->{jail}->error );
|
2018-08-15 20:42:08 +02:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2018-03-08 22:24:02 +01:00
|
|
|
# Enable REST request only if more than 1 2F module is enabled
|
2018-03-08 22:25:56 +01:00
|
|
|
if ( @{ $self->{sfModules} } > 1 ) {
|
2018-09-02 17:31:58 +02:00
|
|
|
$self->addUnauthRoute( '2fchoice' => '_choice', ['POST'] );
|
2018-08-26 22:06:44 +02:00
|
|
|
$self->addUnauthRoute( '2fchoice' => '_redirect', ['GET'] );
|
2018-03-08 22:24:02 +01:00
|
|
|
}
|
|
|
|
|
2018-04-06 16:38:07 +02:00
|
|
|
# Enable 2F registration URL only if at least 1 registration module
|
2018-03-15 07:04:52 +01:00
|
|
|
# is enabled
|
|
|
|
if ( @{ $self->{sfRModules} } ) {
|
|
|
|
|
|
|
|
# Registration base
|
2018-03-22 17:23:48 +01:00
|
|
|
$self->addAuthRoute( '2fregisters' => '_displayRegister', ['GET'] );
|
2018-04-04 23:16:36 +02:00
|
|
|
$self->addAuthRoute( '2fregisters' => 'register', ['POST'] );
|
2018-08-09 08:21:09 +02:00
|
|
|
if ( $self->conf->{sfRequired} ) {
|
|
|
|
$self->addUnauthRoute(
|
|
|
|
'2fregisters' => 'restoreSession',
|
|
|
|
[ 'GET', 'POST' ]
|
|
|
|
);
|
|
|
|
}
|
2018-03-15 07:04:52 +01:00
|
|
|
}
|
|
|
|
|
2018-03-08 16:33:34 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2018-03-08 18:51:01 +01:00
|
|
|
# RUNNING METHODS
|
|
|
|
|
2018-03-22 18:28:36 +01:00
|
|
|
# public PE_CODE run($req)
|
|
|
|
#
|
2018-04-06 16:38:07 +02:00
|
|
|
# run() is called at each authentication, just after sessionInfo populated
|
2018-03-08 18:51:01 +01:00
|
|
|
sub run {
|
2018-03-08 20:36:32 +01:00
|
|
|
my ( $self, $req ) = @_;
|
2018-06-21 21:35:16 +02:00
|
|
|
|
2018-06-12 22:56:25 +02:00
|
|
|
my $checkLogins = $req->param('checkLogins');
|
|
|
|
$self->logger->debug("2F checkLogins set") if ($checkLogins);
|
2018-03-08 18:51:01 +01:00
|
|
|
|
|
|
|
# Skip 2F unless a module has been registered
|
2018-03-08 20:36:32 +01:00
|
|
|
return PE_OK unless ( @{ $self->sfModules } );
|
2018-06-21 21:35:16 +02:00
|
|
|
|
2018-04-06 16:38:07 +02:00
|
|
|
# Search for authorized modules for this user
|
2018-03-08 18:51:01 +01:00
|
|
|
my @am;
|
2018-03-08 20:36:32 +01:00
|
|
|
foreach my $m ( @{ $self->sfModules } ) {
|
|
|
|
$self->logger->debug(
|
|
|
|
'Looking if ' . $m->{m}->prefix . '2F is available' );
|
|
|
|
if ( $m->{r}->( $req, $req->sessionInfo ) ) {
|
2018-03-08 18:51:01 +01:00
|
|
|
$self->logger->debug(' -> OK');
|
2018-03-08 20:36:32 +01:00
|
|
|
push @am, $m->{m};
|
2018-03-08 18:51:01 +01:00
|
|
|
}
|
|
|
|
}
|
2018-06-21 21:35:16 +02:00
|
|
|
|
2018-04-07 13:22:06 +02:00
|
|
|
# If no 2F module is authorized, skipping 2F
|
2018-03-08 18:51:01 +01:00
|
|
|
# Note that a rule may forbid access after (GrantSession plugin)
|
2018-08-09 08:21:09 +02:00
|
|
|
unless (@am) {
|
|
|
|
|
|
|
|
# Except if 2FA is required, move to registration
|
2018-08-15 20:42:08 +02:00
|
|
|
if ( $self->sfReq->( $req, $req->sessionInfo ) ) {
|
2018-08-13 22:12:51 +02:00
|
|
|
$self->logger->debug("2F is required...");
|
2018-08-16 09:39:27 +02:00
|
|
|
$self->logger->debug(" -> Register 2F");
|
2018-10-12 10:04:03 +02:00
|
|
|
$req->pdata->{sfRegToken} =
|
|
|
|
$self->ott->createToken( $req->sessionInfo );
|
2018-08-15 20:42:08 +02:00
|
|
|
$self->logger->debug("Just one 2F is enabled");
|
2018-12-11 22:51:39 +01:00
|
|
|
$self->logger->debug(" -> Redirect to 2fregisters/");
|
2018-08-15 20:42:08 +02:00
|
|
|
$req->response(
|
2018-10-12 10:04:03 +02:00
|
|
|
[
|
|
|
|
302,
|
2018-12-11 22:51:39 +01:00
|
|
|
[ Location => $self->conf->{portal} . '2fregisters/' ], []
|
2018-08-15 20:42:08 +02:00
|
|
|
]
|
|
|
|
);
|
|
|
|
return PE_SENDRESPONSE;
|
2018-08-09 08:21:09 +02:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
return PE_OK;
|
|
|
|
}
|
|
|
|
}
|
2018-03-08 18:51:01 +01:00
|
|
|
|
|
|
|
$self->userLogger->info( 'Second factor required for '
|
2018-10-12 10:04:03 +02:00
|
|
|
. $req->sessionInfo->{ $self->conf->{whatToTrace} } );
|
2018-03-08 20:36:32 +01:00
|
|
|
|
|
|
|
# Store user data in a token
|
2018-03-08 18:51:01 +01:00
|
|
|
$req->sessionInfo->{_2fRealSession} = $req->id;
|
|
|
|
$req->sessionInfo->{_2fUrldc} = $req->urldc;
|
2018-09-12 23:14:35 +02:00
|
|
|
$req->sessionInfo->{_2fUtime} = $req->{sessionInfo}->{_utime};
|
2018-03-08 18:51:01 +01:00
|
|
|
my $token = $self->ott->createToken( $req->sessionInfo );
|
2018-03-09 13:29:39 +01:00
|
|
|
delete $req->{authResult};
|
2018-03-08 20:36:32 +01:00
|
|
|
|
2018-04-06 16:38:07 +02:00
|
|
|
# If only one 2F is authorized, display it
|
2018-03-08 20:36:32 +01:00
|
|
|
unless ($#am) {
|
|
|
|
my $res = $am[0]->run( $req, $token );
|
2018-03-09 13:29:39 +01:00
|
|
|
$req->authResult($res);
|
2018-03-08 18:51:01 +01:00
|
|
|
return $res;
|
|
|
|
}
|
|
|
|
|
|
|
|
# More than 1 2F has been found, display choice
|
2018-03-08 22:24:02 +01:00
|
|
|
$self->logger->debug("Prepare 2F choice");
|
|
|
|
my $tpl = $self->p->sendHtml(
|
|
|
|
$req,
|
|
|
|
'2fchoice',
|
|
|
|
params => {
|
2018-10-12 19:40:13 +02:00
|
|
|
MAIN_LOGO => $self->conf->{portalMainLogo},
|
|
|
|
SKIN => $self->conf->{portalSkin},
|
|
|
|
TOKEN => $token,
|
2018-10-12 10:04:03 +02:00
|
|
|
MODULES => [ map { { CODE => $_->prefix, LOGO => $_->logo } } @am ],
|
2018-06-12 22:56:25 +02:00
|
|
|
CHECKLOGINS => $checkLogins
|
2018-03-08 22:24:02 +01:00
|
|
|
}
|
|
|
|
);
|
2018-03-09 13:29:39 +01:00
|
|
|
$req->response($tpl);
|
|
|
|
return PE_SENDRESPONSE;
|
2018-03-08 22:24:02 +01:00
|
|
|
}
|
|
|
|
|
2018-03-26 10:33:04 +02:00
|
|
|
# bool public display2fRegisters($req, $session)
|
2018-03-22 18:28:36 +01:00
|
|
|
#
|
|
|
|
# Return true if at least 1 register module is available for this user. Used
|
|
|
|
# by Menu to display or not /2fregisters page
|
2018-03-26 10:33:04 +02:00
|
|
|
sub display2fRegisters {
|
2018-04-04 23:16:36 +02:00
|
|
|
my ( $self, $req, $session ) = @_;
|
2018-03-22 18:28:36 +01:00
|
|
|
foreach my $m ( @{ $self->sfRModules } ) {
|
2018-04-04 23:16:36 +02:00
|
|
|
return 1 if ( $m->{r}->( $req, $session ) );
|
2018-03-22 18:28:36 +01:00
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2018-10-12 21:13:12 +02:00
|
|
|
sub _choice {
|
|
|
|
my ( $self, $req ) = @_;
|
|
|
|
my $token;
|
2018-03-09 13:29:39 +01:00
|
|
|
|
2018-10-12 21:13:12 +02:00
|
|
|
# Restore session
|
|
|
|
unless ( $token = $req->param('token') ) {
|
|
|
|
$self->userLogger->error( $self->prefix . ' 2F access without token' );
|
|
|
|
$req->mustRedirect(1);
|
|
|
|
return $self->p->do( $req, [ sub { PE_NOTOKEN } ] );
|
|
|
|
}
|
2018-03-09 13:29:39 +01:00
|
|
|
|
2018-10-12 21:13:12 +02:00
|
|
|
my $session;
|
|
|
|
unless ( $session = $self->ott->getToken($token) ) {
|
|
|
|
$self->userLogger->info('Token expired');
|
|
|
|
return $self->p->do( $req, [ sub { PE_TOKENEXPIRED } ] );
|
|
|
|
}
|
2018-03-09 13:29:39 +01:00
|
|
|
|
2018-10-12 21:13:12 +02:00
|
|
|
$req->sessionInfo($session);
|
2018-03-09 13:29:39 +01:00
|
|
|
|
2018-10-12 21:13:12 +02:00
|
|
|
# New token
|
|
|
|
$token = $self->ott->createToken($session);
|
2018-03-09 13:29:39 +01:00
|
|
|
|
2018-10-12 21:13:12 +02:00
|
|
|
my $ch = $req->param('sf');
|
|
|
|
foreach my $m ( @{ $self->sfModules } ) {
|
|
|
|
if ( $m->{m}->prefix eq $ch ) {
|
|
|
|
my $res = $m->{m}->run( $req, $token );
|
|
|
|
$req->authResult($res);
|
|
|
|
return $self->p->do(
|
|
|
|
$req,
|
|
|
|
[
|
|
|
|
sub { $res }, 'controlUrl',
|
|
|
|
'buildCookie', @{ $self->p->endAuth },
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$self->userLogger->error('Bad 2F choice');
|
|
|
|
return $self->p->lmError( $req, 500 );
|
|
|
|
}
|
2018-03-08 18:51:01 +01:00
|
|
|
|
2018-10-12 21:13:12 +02:00
|
|
|
sub _redirect {
|
|
|
|
my ( $self, $req ) = @_;
|
|
|
|
my $arg = $req->env->{QUERY_STRING};
|
|
|
|
$self->logger->debug('Call sfEngine _redirect method');
|
|
|
|
return [
|
|
|
|
302,
|
|
|
|
[
|
|
|
|
Location => $self->conf->{portal} . ( $arg ? "?$arg" : '' )
|
|
|
|
],
|
|
|
|
[]
|
|
|
|
];
|
|
|
|
}
|
2018-03-15 07:04:52 +01:00
|
|
|
|
2018-10-12 21:13:12 +02:00
|
|
|
sub _displayRegister {
|
|
|
|
my ( $self, $req, $tpl ) = @_;
|
|
|
|
|
|
|
|
# After verifying rule:
|
|
|
|
# - display template if $tpl
|
|
|
|
# - else display choice template
|
|
|
|
if ($tpl) {
|
|
|
|
my ($m) =
|
|
|
|
grep { $_->{m}->prefix eq $tpl } @{ $self->sfRModules };
|
|
|
|
unless ($m) {
|
|
|
|
return $self->p->sendError( $req,
|
|
|
|
'Inexistent register module', 400 );
|
|
|
|
}
|
|
|
|
unless ( $m->{r}->( $req, $req->userData ) ) {
|
|
|
|
return $self->p->sendError( $req,
|
|
|
|
'Registration not authorized', 403 );
|
|
|
|
}
|
|
|
|
return $self->p->sendHtml( $req, $m->{m}->template,
|
|
|
|
params => { MAIN_LOGO => $self->conf->{portalMainLogo} } );
|
|
|
|
}
|
|
|
|
|
|
|
|
# If only one 2F is available, redirect to it
|
|
|
|
my @am;
|
|
|
|
foreach my $m ( @{ $self->sfRModules } ) {
|
|
|
|
$self->logger->debug(
|
|
|
|
'Looking if ' . $m->{m}->prefix . '2F register is available' );
|
|
|
|
if ( $m->{r}->( $req, $req->userData ) ) {
|
|
|
|
push @am,
|
|
|
|
{
|
|
|
|
CODE => $m->{m}->prefix,
|
|
|
|
URL => '/2fregisters/' . $m->{m}->prefix,
|
|
|
|
LOGO => $m->{m}->logo,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
@am == 1
|
|
|
|
and not( $req->userData->{_2fDevices}
|
|
|
|
or $req->data->{sfRegRequired} )
|
|
|
|
)
|
|
|
|
{
|
|
|
|
return [ 302, [ Location => $self->conf->{portal} . $am[0]->{URL} ],
|
|
|
|
[] ];
|
|
|
|
}
|
2018-04-04 23:16:36 +02:00
|
|
|
|
2018-10-12 21:13:12 +02:00
|
|
|
my $_2fDevices =
|
|
|
|
$req->userData->{_2fDevices}
|
|
|
|
? eval { from_json( $req->userData->{_2fDevices},
|
|
|
|
{ allow_nonref => 1 } ); }
|
|
|
|
: undef;
|
2018-04-04 23:16:36 +02:00
|
|
|
|
2018-10-12 21:13:12 +02:00
|
|
|
unless ($_2fDevices) {
|
|
|
|
$self->logger->debug("No 2F Device found");
|
|
|
|
$_2fDevices = [];
|
|
|
|
}
|
2018-04-04 23:16:36 +02:00
|
|
|
|
2018-10-12 21:13:12 +02:00
|
|
|
return $self->p->sendHtml(
|
|
|
|
$req,
|
|
|
|
'2fregisters',
|
|
|
|
params => {
|
|
|
|
MAIN_LOGO => $self->conf->{portalMainLogo},
|
|
|
|
SKIN => $self->conf->{portalSkin},
|
|
|
|
MODULES => \@am,
|
|
|
|
SFDEVICES => $_2fDevices,
|
|
|
|
REG_REQUIRED => $req->data->{sfRegRequired},
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
2018-03-15 07:04:52 +01:00
|
|
|
|
2018-10-12 21:13:12 +02:00
|
|
|
# Check rule and display
|
|
|
|
sub register {
|
|
|
|
my ( $self, $req, $tpl, @args ) = @_;
|
|
|
|
|
|
|
|
# After verifying rule:
|
|
|
|
# - call register run method if $tpl
|
|
|
|
# - else give JSON list of available registers for this user
|
|
|
|
if ($tpl) {
|
|
|
|
my ($m) =
|
|
|
|
grep { $_->{m}->prefix eq $tpl } @{ $self->sfRModules };
|
|
|
|
unless ($m) {
|
|
|
|
return $self->p->sendError( $req,
|
|
|
|
'Inexistent register module', 400 );
|
|
|
|
}
|
|
|
|
unless ( $m->{r}->( $req, $req->userData ) ) {
|
|
|
|
$self->userLogger->error("$tpl 2F registration refused");
|
|
|
|
return $self->p->sendError( $req, 'Registration refused', 403 );
|
|
|
|
}
|
|
|
|
return $m->{m}->run( $req, @args );
|
|
|
|
}
|
|
|
|
my @am;
|
|
|
|
foreach my $m ( @{ $self->sfRModules } ) {
|
|
|
|
$self->logger->debug(
|
|
|
|
'Looking if ' . $m->{m}->prefix . '2F register is available' );
|
|
|
|
if ( $m->{r}->( $req, $req->userData ) ) {
|
|
|
|
$self->logger->debug(' -> OK');
|
|
|
|
my $name = $m->{m}->prefix;
|
|
|
|
push @am,
|
|
|
|
{
|
|
|
|
name => $name,
|
|
|
|
logo => $m->{m}->logo,
|
|
|
|
url => "/2fregisters/$name"
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $self->p->sendJSONresponse( $req, \@am );
|
|
|
|
}
|
2018-03-15 07:04:52 +01:00
|
|
|
|
2018-10-12 21:13:12 +02:00
|
|
|
sub restoreSession {
|
|
|
|
my ( $self, $req, @path ) = @_;
|
|
|
|
my $token = $req->pdata->{sfRegToken}
|
|
|
|
or return [ 302, [ Location => $self->conf->{portal} ], [] ];
|
|
|
|
$req->userData( $self->ott->getToken( $token, 1 ) );
|
|
|
|
$req->data->{sfRegRequired} = 1;
|
|
|
|
return $req->method eq 'POST'
|
|
|
|
? $self->register( $req, @path )
|
|
|
|
: $self->_displayRegister( $req, @path );
|
|
|
|
}
|
2018-08-09 08:21:09 +02:00
|
|
|
|
2018-10-12 21:13:12 +02:00
|
|
|
1;
|