File Coverage

blib/lib/AuthCAS.pm
Criterion Covered Total %
statement 81 206 39.3
branch 20 66 30.3
condition 16 33 48.4
subroutine 11 22 50.0
pod 16 16 100.0
total 144 343 41.9


line stmt bran cond sub pod time code
1              
2             package AuthCAS;
3              
4 2     2   56759 use strict;
  2         5  
  2         83  
5 2     2   11 use vars qw( $VERSION);
  2         5  
  2         200  
6              
7             $VERSION = '1.6';
8              
9             =head1 NAME
10              
11             AuthCAS - Client library for JA-SIG CAS 2.0 authentication server
12              
13             =head1 VERSION
14              
15             Version 1.6
16              
17             =head1 DESCRIPTION
18              
19             AuthCAS aims at providing a Perl API to JA-SIG Central Authentication System (CAS).
20             Only a basic Perl library is provided with CAS whereas AuthCAS is a full object-oriented library.
21              
22             =head1 PREREQUISITES
23              
24             This script requires IO::Socket::SSL and LWP::UserAgent
25              
26             =head1 SYNOPSIS
27              
28             A simple example with a direct CAS authentication
29              
30             use AuthCAS;
31             my $cas = new AuthCAS(casUrl => 'https://cas.myserver,
32             CAFile => '/etc/httpd/conf/ssl.crt/ca-bundle.crt',
33             );
34              
35             my $login_url = $cas->getServerLoginURL('http://myserver/app.cgi');
36              
37             ## The user should be redirected to the $login_url
38             ## When coming back from the CAS server a ticket is provided in the QUERY_STRING
39              
40             ## $ST should contain the receaved Service Ticket
41             my $user = $cas->validateST('http://myserver/app.cgi', $ST);
42              
43             printf "User authenticated as %s\n", $user;
44              
45              
46             In the following example a proxy is requesting a Proxy Ticket for the target application
47              
48             $cas->proxyMode(pgtFile => '/tmp/pgt.txt',
49             pgtCallbackUrl => 'https://myserver/proxy.cgi?callback=1
50             );
51            
52             ## Same as before but the URL is the proxy URL
53             my $login_url = $cas->getServerLoginURL('http://myserver/proxy.cgi');
54              
55             ## Like in the previous example we should receave a $ST
56              
57             my $user = $cas->validateST('http://myserver/proxy.cgi', $ST);
58              
59             ## Process errors
60             printf STDERR "Error: %s\n", &AuthCAS::get_errors() unless (defined $user);
61              
62             ## Now we request a Proxy Ticket for the target application
63             my $PT = $cas->retrievePT('http://myserver/app.cgi');
64            
65             ## This piece of code is executed by the target application
66             ## It received a Proxy Ticket from the proxy
67             my ($user, @proxies) = $cas->validatePT('http://myserver/app.cgi', $PT);
68              
69             printf "User authenticated as %s via %s proxies\n", $user, join(',',@proxies);
70              
71              
72             =head1 DESCRIPTION
73              
74             Jasig CAS is Yale University's web authentication system, heavily inspired by Kerberos.
75             Release 2.0 of CAS provides "proxied credential" feature that allows authentication
76             tickets to be carried by intermediate applications (Portals for instance), they are
77             called proxy.
78              
79             This AuthCAS Perl module provides required subroutines to validate and retrieve CAS tickets.
80              
81             =cut
82              
83             my @ISA = qw(Exporter);
84             my @EXPORT = qw($errors);
85              
86             my $errors;
87              
88 2     2   11 use Carp;
  2         9  
  2         5987  
89              
90             =pod
91              
92             =head2 new
93              
94             my $cas = new AuthCAS(
95             casUrl => 'https://cas.myserver',
96             CAFile => '/etc/httpd/conf/ssl.crt/ca-bundle.crt',
97             );
98              
99             The C constructor lets you create a new B object.
100              
101             =over
102              
103             =item casUrl - REQUIRED
104              
105             =item CAFile
106              
107             =item CAPath
108              
109             =item loginPath - '/login'
110              
111             =item logoutPath - '/logout'
112              
113             =item serviceValidatePath - '/serviceValidate'
114              
115             =item proxyPath - '/proxy'
116              
117             =item proxyValidatePath - '/proxyValidate'
118              
119             =item SSL_version - unset
120              
121             Sets the version of the SSL protocol used to transmit data. If the default causes connection issues, setting it to 'SSLv3' may help.
122             see the documentation for L for more information
123             see L for more details.
124              
125             =back
126              
127             Returns a new B or dies on error.
128              
129             =cut
130              
131             sub new {
132 2     2 1 784 my ( $pkg, %param ) = @_;
133 2   50     48 my $cas_server = {
      50        
      50        
      50        
      50        
134             url => $param{'casUrl'},
135             CAFile => $param{'CAFile'},
136             CAPath => $param{'CAPath'},
137              
138             loginPath => $param{'loginPath'} || '/login',
139             logoutPath => $param{'logoutPath'} || '/logout',
140             serviceValidatePath => $param{'serviceValidatePath'}
141             || '/serviceValidate',
142             proxyPath => $param{'proxyPath'} || '/proxy',
143             proxyValidatePath => $param{'proxyValidatePath'} || '/proxyValidate',
144             SSL_version => $param{SSL_version},
145             };
146              
147 2         5 bless $cas_server, $pkg;
148              
149 2         6 return $cas_server;
150             }
151              
152             =pod
153              
154             =head2 get_errors
155              
156             Return module errors
157              
158             =cut
159              
160             sub get_errors {
161 0     0 1 0 return $errors;
162             }
163              
164             =pod
165              
166             =head2 proxyMode
167              
168             Use the CAS object as a proxy
169              
170              
171             =over
172              
173             =item pgtFile
174             =item pgtCallbackUrl
175              
176             =back
177              
178              
179             =cut
180              
181             sub proxyMode {
182 0     0 1 0 my $self = shift;
183 0         0 my %param = @_;
184              
185 0         0 $self->{'pgtFile'} = $param{'pgtFile'};
186 0         0 $self->{'pgtCallbackUrl'} = $param{'pgtCallbackUrl'};
187 0         0 $self->{'proxy'} = 1;
188              
189 0         0 return 1;
190             }
191              
192             ## Escape dangerous chars in URLS
193             sub _escape_chars {
194 5     5   9 my $s = shift;
195              
196             ## Escape chars
197             ## !"#$%&'()+,:;<=>?[] AND accented chars
198             ## escape % first
199             # foreach my $i (0x25,0x20..0x24,0x26..0x2c,0x3a..0x3f,0x5b,0x5d,0x80..0x9f,0xa0..0xff) {
200 5         8 foreach my $i (0x26) {
201 5         16 my $hex_i = sprintf "%lx", $i;
202 5         34 $s =~ s/\x$hex_i/%$hex_i/g;
203             }
204              
205 5         18 return $s;
206             }
207              
208             =pod
209              
210             =head2 dump_var
211              
212              
213             =cut
214              
215             sub dump_var {
216 0     0 1 0 my ( $var, $level, $fd ) = @_;
217              
218 0 0       0 if ( ref($var) ) {
219 0 0       0 if ( ref($var) eq 'ARRAY' ) {
    0          
220 0         0 foreach my $index ( 0 .. $#{$var} ) {
  0         0  
221 0         0 print $fd "\t" x $level . $index . "\n";
222 0         0 &dump_var( $var->[$index], $level + 1, $fd );
223             }
224             }
225             elsif ( ref($var) eq 'HASH' ) {
226 0         0 foreach my $key ( sort keys %{$var} ) {
  0         0  
227 0         0 print $fd "\t" x $level . '_' . $key . '_' . "\n";
228 0         0 &dump_var( $var->{$key}, $level + 1, $fd );
229             }
230             }
231             }
232             else {
233 0 0       0 if ( defined $var ) {
234 0         0 print $fd "\t" x $level . "'$var'" . "\n";
235             }
236             else {
237 0         0 print $fd "\t" x $level . "UNDEF\n";
238             }
239             }
240             }
241              
242             ## Parse an HTTP URL
243             sub _parse_url {
244 3     3   4 my $url = shift;
245              
246 3         4 my ( $host, $port, $path );
247              
248 3 50       23 if ( $url =~ /^(https?):\/\/([^:\/]+)(:(\d+))?(.*)$/ ) {
249 3         11 $host = $2;
250 3         5 $path = $5;
251 3 50       13 if ( $1 eq 'http' ) {
    50          
252 0   0     0 $port = $4 || 80;
253             }
254             elsif ( $1 eq 'https' ) {
255 3   100     26 $port = $4 || 443;
256             }
257             else {
258 0         0 $errors = sprintf "Unknown protocol '%s'\n", $1;
259 0         0 return undef;
260             }
261             }
262             else {
263 0         0 $errors = sprintf "Unable to parse URL '%s'\n", $url;
264 0         0 return undef;
265             }
266              
267 3         10 return ( $host, $port, $path );
268             }
269              
270             ## Simple XML parser
271             sub _parse_xml {
272 0     0   0 my $data = shift;
273              
274 0         0 my %xml_struct;
275              
276 0         0 while ( $data =~ /^<([^\s>]+)(\s+[^\s>]+)*>([\s\S\n]*)(<\/\1>)/m ) {
277 0         0 my ( $new_tag, $new_data ) = ( $1, $3 );
278 0         0 chomp $new_data;
279 0         0 $new_data =~ s/^[\s\n]+//m;
280 0         0 $data =~ s/^<$new_tag(\s+[^\s>]+)*>([\s\S\n]*)(<\/$new_tag>)//m;
281 0         0 $data =~ s/^[\s\n]+//m;
282              
283             ## Check if data still includes XML tags
284 0         0 my $struct;
285 0 0       0 if ( $new_data =~ /^<([^\s>]+)(\s+[^\s>]+)*>([\s\S\n]*)(<\/\1>)/m ) {
286 0         0 $struct = &_parse_xml($new_data);
287             }
288             else {
289 0         0 $struct = $new_data;
290             }
291 0         0 push @{ $xml_struct{$new_tag} }, $struct;
  0         0  
292             }
293              
294 0         0 return \%xml_struct;
295             }
296              
297             =pod
298              
299             =head2 getServerLoginURL($service)
300              
301             Returns a URL that you can redirect the browser to, which includes the URL to return to
302              
303             TODO: it escapes the return URL, but I've noticed some issues with more complicated URL's
304              
305             =cut
306              
307             sub getServerLoginURL {
308 2     2 1 9 my $self = shift;
309 2         3 my $service = shift;
310              
311             return
312 2         12 $self->{'url'}
313             . $self->{'loginPath'}
314             . '?service='
315             . &_escape_chars($service);
316             }
317              
318             =pod
319              
320             =head2 getServerLoginGatewayURL($service)
321              
322             Returns non-blocking login URL
323             ie: if user is logged in, return the ticket, otherwise do not prompt for login
324              
325             =cut
326              
327             sub getServerLoginGatewayURL {
328 0     0 1 0 my $self = shift;
329 0         0 my $service = shift;
330              
331             return
332 0         0 $self->{'url'}
333             . $self->{'loginPath'}
334             . '?service='
335             . &_escape_chars($service)
336             . '&gateway=1';
337             }
338              
339             =pod
340              
341             =head2 getServerLogoutURL($service)
342              
343             Return logout URL
344             After logout user is redirected back to the application
345              
346             =cut
347              
348             sub getServerLogoutURL {
349 0     0 1 0 my $self = shift;
350 0         0 my $service = shift;
351              
352             return
353 0         0 $self->{'url'}
354             . $self->{'logoutPath'}
355             . '?service='
356             . &_escape_chars($service)
357             . '&gateway=1';
358             }
359              
360             =pod
361              
362             =head2 getServerServiceValidateURL($service, $ticket, $pgtUrl)
363              
364             Returns
365              
366             =cut
367              
368             sub getServerServiceValidateURL {
369 3     3 1 4 my $self = shift;
370 3         5 my $service = shift;
371 3         4 my $ticket = shift;
372 3         7 my $pgtUrl = shift;
373              
374 3         7 my $query_string =
375             'service=' . &_escape_chars($service) . '&ticket=' . $ticket;
376 3 50       9 if ( defined $pgtUrl ) {
377 0         0 $query_string .= '&pgtUrl=' . &_escape_chars($pgtUrl);
378             }
379              
380             ## URL was /validate with CAS 1.0
381             return
382 3         18 $self->{'url'}
383             . $self->{'serviceValidatePath'} . '?'
384             . $query_string;
385             }
386              
387             =pod
388              
389             =head2 getServerProxyURL($targetService, $pgt)
390              
391             Returns
392              
393             =cut
394              
395             sub getServerProxyURL {
396 0     0 1 0 my $self = shift;
397 0         0 my $targetService = shift;
398 0         0 my $pgt = shift;
399              
400             return
401 0         0 $self->{'url'}
402             . $self->{'proxyPath'}
403             . '?targetService='
404             . &_escape_chars($targetService) . '&pgt='
405             . &_escape_chars($pgt);
406             }
407              
408             =pod
409              
410             =head2 getServerProxyValidateURL($service, $ticket)
411              
412             Returns
413              
414             =cut
415              
416             sub getServerProxyValidateURL {
417 0     0 1 0 my $self = shift;
418 0         0 my $service = shift;
419 0         0 my $ticket = shift;
420              
421             return
422 0         0 $self->{'url'}
423             . $self->{'proxyValidatePath'}
424             . '?service='
425             . &_escape_chars($service)
426             . '&ticket='
427             . &_escape_chars($ticket);
428              
429             }
430              
431             =pod
432              
433             =head2 validateST($service, $ticket)
434              
435             Validate a Service Ticket
436             Also used to get a PGT
437              
438              
439             Returns the login that created the ticket, if the ticket is valid for that $service URL
440              
441             returns undef if the ticket is not valid.
442              
443             =cut
444              
445             sub validateST {
446 5     5 1 20 my $self = shift;
447 5         8 my $service = shift;
448 5         9 my $ticket = shift;
449            
450 5 100       14 if (!defined($service)) {
451 1         3 $errors = 'Need a service url to validate ticket.';
452 1         5 return undef;
453             }
454 4 100       11 if (!defined($ticket)) {
455 1         2 $errors = 'No ticket to validate.';
456 1         5 return undef;
457             }
458              
459 3         8 my $pgtUrl = $self->{'pgtCallbackUrl'};
460              
461 3         11 my $xml =
462             $self->callCAS(
463             $self->getServerServiceValidateURL( $service, $ticket, $pgtUrl ) );
464              
465 3 50       20 if ( defined $xml->{'cas:serviceResponse'}[0]{'cas:authenticationFailure'} )
466             {
467 0         0 $errors = sprintf "Failed to validate Service Ticket %s : %s\n",
468             $ticket,
469             $xml->{'cas:serviceResponse'}[0]{'cas:authenticationFailure'}[0];
470 0         0 return undef;
471             }
472              
473 3         17 my $user =
474             $xml->{'cas:serviceResponse'}[0]{'cas:authenticationSuccess'}[0]
475             {'cas:user'}[0];
476              
477             ## If in Proxy mode, also retreave a PGT
478 3 50       15 if ( $self->{'proxy'} ) {
479 0         0 my $pgtIou;
480 0 0       0 if (
481             defined $xml->{'cas:serviceResponse'}[0]
482             {'cas:authenticationSuccess'}[0]{'cas:proxyGrantingTicket'} )
483             {
484 0         0 $pgtIou =
485             $xml->{'cas:serviceResponse'}[0]{'cas:authenticationSuccess'}[0]
486             {'cas:proxyGrantingTicket'}[0];
487             }
488              
489 0 0       0 unless ( defined $self->{'pgtFile'} ) {
490 0         0 $errors = sprintf "pgtFile not defined\n";
491 0         0 return undef;
492             }
493              
494             ## Check stored PGT
495 0 0       0 unless ( open STORE, $self->{'pgtFile'} ) {
496 0         0 $errors = sprintf "Unable to read %s\n", $self->{'pgtFile'};
497 0         0 return undef;
498             }
499              
500 0         0 my $pgtId;
501 0         0 while () {
502 0 0       0 if (/^$pgtIou\s+(.+)$/) {
503 0         0 $pgtId = $1;
504 0         0 last;
505             }
506             }
507              
508 0         0 $self->{'pgtId'} = $pgtId;
509             }
510              
511 3         26 return ($user);
512             }
513              
514             =pod
515              
516             =head2 validatePT($service, $ticket)
517              
518             Validate a Proxy Ticket
519              
520             Returns the login that created the ticket, if the ticket is valid for that $service URL,
521             and a list of Proxies used.
522            
523             user returned == undef if its not a valid ticket
524              
525             =cut
526              
527             sub validatePT {
528 0     0 1 0 my $self = shift;
529 0         0 my $service = shift;
530 0         0 my $ticket = shift;
531              
532 0 0       0 if (!defined($service)) {
533 0         0 $errors = 'Need a service url to validate ticket.';
534 0         0 return undef;
535             }
536 0 0       0 if (!defined($ticket)) {
537 0         0 $errors = 'No ticket to validate.';
538 0         0 return undef;
539             }
540              
541 0         0 my $xml =
542             $self->callCAS( $self->getServerProxyValidateURL( $service, $ticket ) );
543              
544 0 0       0 if ( defined $xml->{'cas:serviceResponse'}[0]{'cas:authenticationFailure'} )
545             {
546 0         0 $errors = sprintf "Failed to validate Proxy Ticket %s : %s\n", $ticket,
547             $xml->{'cas:serviceResponse'}[0]{'cas:authenticationFailure'}[0];
548 0         0 return undef;
549             }
550              
551 0         0 my $user =
552             $xml->{'cas:serviceResponse'}[0]{'cas:authenticationSuccess'}[0]
553             {'cas:user'}[0];
554              
555 0         0 my @proxies;
556 0 0       0 if (
557             defined $xml->{'cas:serviceResponse'}[0]{'cas:authenticationSuccess'}[0]
558             {'cas:proxies'} )
559             {
560 0         0 @proxies =
561 0         0 @{ $xml->{'cas:serviceResponse'}[0]{'cas:authenticationSuccess'}[0]
562             {'cas:proxies'}[0]{'cas:proxy'} };
563             }
564              
565 0         0 return ( $user, @proxies );
566             }
567              
568             =pod
569              
570             =head2 callCAS($url)
571              
572             ## Access a CAS URL and parses received XML
573              
574             Returns
575              
576             =cut
577              
578             sub callCAS {
579 3     3 1 5 my $self = shift;
580 3         3 my $url = shift;
581              
582 3         10 my ( $host, $port, $path ) = &_parse_url($url);
583              
584 3         22 my $xml = get_https2(
585             $host, $port, $path,
586             {
587             'cafile' => $self->{'CAFile'},
588             'capath' => $self->{'CAPath'},
589             'SSL_version' => $self->{'SSL_version'}
590             }
591             );
592              
593             #use Data::Dumper; die '--'.$#$xml.': '.Dumper($xml);
594              
595 3 50 33     16 unless ($xml && $#$xml >= 0) {
596 3         266 warn $errors;
597 3         13 return undef;
598             }
599              
600             ## Skip HTTP header fields
601 0         0 my $line = shift @$xml;
602 0         0 while ( $line !~ /^\s*$/ ) {
603 0         0 $line = shift @$xml;
604             }
605              
606 0         0 return &_parse_xml( join( '', @$xml ) );
607             }
608              
609             =pod
610              
611             =head2 storePGT($pgtIou, $pgtId)
612              
613             =cut
614              
615             sub storePGT {
616 0     0 1 0 my $self = shift;
617 0         0 my $pgtIou = shift;
618 0         0 my $pgtId = shift;
619              
620 0 0       0 unless ( open STORE, ">>$self->{'pgtFile'}" ) {
621 0         0 $errors = sprintf "Unable to write to %s\n", $self->{'pgtFile'};
622 0         0 return undef;
623             }
624 0         0 printf STORE "%s\t%s\n", $pgtIou, $pgtId;
625 0         0 close STORE;
626              
627 0         0 return 1;
628             }
629              
630             =pod
631              
632             =head2 retrievePT($service)
633              
634             Returns
635              
636             =cut
637              
638             sub retrievePT {
639 0     0 1 0 my $self = shift;
640 0         0 my $service = shift;
641              
642 0         0 my $xml =
643             $self->callCAS( $self->getServerProxyURL( $service, $self->{'pgtId'} ) );
644              
645 0 0       0 if ( defined $xml->{'cas:serviceResponse'}[0]{'cas:proxyFailure'} ) {
646 0         0 $errors = sprintf "Failed to get PT : %s\n",
647             $xml->{'cas:serviceResponse'}[0]{'cas:proxyFailure'}[0];
648 0         0 return undef;
649             }
650              
651 0 0       0 if (
652             defined $xml->{'cas:serviceResponse'}[0]{'cas:proxySuccess'}[0]
653             {'cas:proxyTicket'} )
654             {
655 0         0 return $xml->{'cas:serviceResponse'}[0]{'cas:proxySuccess'}[0]
656             {'cas:proxyTicket'}[0];
657             }
658              
659 0         0 return undef;
660             }
661              
662             =pod
663              
664             =head2 get_https2
665              
666             request a document using https, return status and content
667              
668             Sven suspects this is intended to be private.
669              
670             Returns
671              
672             =cut
673              
674             sub get_https2 {
675 3     3 1 5 my $host = shift;
676 3         536 my $port = shift;
677 3         4 my $path = shift;
678              
679 3         5 my $ssl_data = shift;
680              
681 3         6 my $trusted_ca_file = $ssl_data->{'cafile'};
682 3         5 my $trusted_ca_path = $ssl_data->{'capath'};
683              
684 3 100 66     64 if ( ( $trusted_ca_file && !( -r $trusted_ca_file ) )
      33        
      66        
685             || ( $trusted_ca_path && !( -d $trusted_ca_path ) ) )
686             {
687 1   50     10 $errors = sprintf
      50        
688             "error : incorrect access to cafile ".($trusted_ca_file||'')." or capath ".($trusted_ca_path||'')."\n";
689 1         3 return undef;
690             }
691              
692 2 50       166 unless ( eval "require IO::Socket::SSL" ) {
693 0         0 $errors = sprintf
694             "Unable to use SSL library, IO::Socket::SSL required, install IO-Socket-SSL (CPAN) first\n";
695 0         0 return undef;
696             }
697 2         99621 require IO::Socket::SSL;
698              
699 2 50       317 unless ( eval "require LWP::UserAgent" ) {
700 0         0 $errors = sprintf
701             "Unable to use LWP library, LWP::UserAgent required, install LWP (CPAN) first\n";
702 0         0 return undef;
703             }
704 2         50223 require LWP::UserAgent;
705              
706 2         4 my $ssl_socket;
707              
708 2         16 my %ssl_options = (
709             SSL_use_cert => 0,
710             PeerAddr => $host,
711             PeerPort => $port,
712             Proto => 'tcp',
713             Timeout => '5'
714             );
715              
716 2 50       7 $ssl_options{'SSL_ca_file'} = $trusted_ca_file if ($trusted_ca_file);
717 2 50       9 $ssl_options{'SSL_ca_path'} = $trusted_ca_path if ($trusted_ca_path);
718              
719             ## If SSL_ca_file or SSL_ca_path => verify peer certificate
720 2 50 33     20 $ssl_options{'SSL_verify_mode'} = 0x01
721             if ( $trusted_ca_file || $trusted_ca_path );
722              
723 2 50       13 $ssl_options{'SSL_version'} = $ssl_data->{'SSL_version'}
724             if defined( $ssl_data->{'SSL_version'} );
725              
726 2         31 $ssl_socket = new IO::Socket::SSL(%ssl_options);
727              
728 2 50       7521 unless ($ssl_socket) {
729 2         12 $errors = sprintf "error %s unable to connect https://%s:%s/\n",
730             &IO::Socket::SSL::errstr, $host, $port;
731 2         35 return undef;
732             }
733              
734 0           my $request = "GET $path HTTP/1.0\r\nHost: $host\r\n\r\n";
735 0           print $ssl_socket "$request";
736              
737 0           my @result;
738 0           while ( my $line = $ssl_socket->getline ) {
739 0           push @result, $line;
740             }
741              
742 0           $ssl_socket->close( SSL_no_shutdown => 1 );
743              
744 0           return \@result;
745             }
746              
747             =pod
748              
749             =head1 SEE ALSO
750              
751             JA-SIG Central Authentication Service L
752              
753             was Yale Central Authentication Service L
754            
755             phpCAS L
756              
757             =head1 COPYRIGHT
758              
759             Copyright (C) 2003, 2005,2006,2007,2009 Olivier Salaun - Comité Réseau des Universités L
760             2012 Sven Dowideit - L
761              
762              
763             This library is free software; you can redistribute it and/or modify
764             it under the same terms as Perl itself.
765              
766             =head1 AUTHORS
767              
768             Olivier Salaun
769             Sven Dowideit
770              
771             =cut
772              
773             1;
774