]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Utils/RemoteAccount.pm
cd77276e0abd5fe8ab710dabe78c4a0a90add85c
[working/Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Utils / RemoteAccount.pm
1 package OpenILS::Utils::RemoteAccount;
2
3 # use OpenSRF::Utils::SettingsClient;
4 use OpenSRF::Utils::Logger qw/:logger/;
5
6 use Data::Dumper;
7 use Net::uFTP;
8 use Net::SSH2;      # because uFTP doesn't handle SSH keys (yet?)
9 use File::Temp;
10 use File::Basename;
11 use File::Spec;
12 use Text::Glob qw( match_glob glob_to_regex );
13 # use Error;
14
15 $Data::Dumper::Indent = 0;
16
17 use strict;
18 use warnings;
19
20 use Carp;
21
22 our $AUTOLOAD;
23
24 our %keyfiles = ();
25
26 my %fields = (
27     account_object  => undef,
28     remote_host     => undef,
29     remote_user     => undef,
30     remote_password => undef,
31     remote_account  => undef,
32     remote_file     => undef,
33     remote_path     => undef,   # not really doing anything with this... yet.
34     ssh_privatekey  => undef,
35     ssh_publickey   => undef,
36     type            => undef,
37     port            => undef,
38     content         => undef,
39     local_file      => undef,
40     tempfile        => undef,
41     error           => undef,
42     single_ext      => undef,
43     specific        => 0,
44     debug           => 0,
45 );
46
47
48 =head1 NAME 
49
50 OpenILS::Utils::RemoteAccount - Encapsulate FTP, SFTP and SSH file transactions for Evergreen
51
52 =head1 DESCRIPTION
53
54 The Remote Account module attempts to transfer a file to/from a remote server.
55 Net::uFTP is used, encapsulating the available options of SCP, FTP and SFTP.
56
57 =head1 PARAMETERS
58
59 All information is expected to be supplied by the caller via parameters:
60    ~ remote_host (required)
61    ~ remote_user
62    ~ remote_password
63    ~ remote_account
64    ~ ssh_privatekey
65    ~ ssh_publickey
66    ~ type (FTP, SFTP or SCP -- default FTP)
67    ~ port
68    ~ debug
69
70 The latter three are optionally passed to the Net::uFTP constructor.
71
72 Note: none of the parameters are actually required, except remote_host.
73 That is because remote_user, remote_password and remote_account can all be 
74 extrapolated from other sources, as the Net::FTP docs describe:
75
76     If no arguments are given then Net::FTP uses the Net::Netrc package
77         to lookup the login information for the connected host.
78
79     If no information is found then a login of anonymous is used.
80
81     If no password is given and the login is anonymous then anonymous@
82         will be used for password.
83
84 Note that specifying a password will require you to specify a user.
85 Similarly, specifying an account requires both user and password.
86 That is, there are no assumed defaults when the latter arguments are used.
87
88 =head2 SSH KEYS:
89
90 The use of ssh keys is preferred.  Explicit speicification of connection type will prevent
91 multiple attempts to the same server.  Therefore, using the type parameter is also recommended.
92
93 If the type is not explicit, we attempt to use SSH keys where they are specified or otherwise found
94 in the runtime environment.  If only one key is specified, we attempt to derive
95 the corresponding filename based on the ssh-keygen defaults.  If either key is
96 specified, but both are not found (and readable) then the result is failure.  If
97 no key or type is specified, but keys are found, the key-based connections will be attempted,
98 but failure will be non-fatal.
99
100 =cut
101
102 sub plausible_dirs {
103     # returns plausible locations of a .ssh subdir where SSH keys might be stashed
104     # NOTE: these would need to be properly genericized w/ Makefule vars
105     # in order to support Debian packaging and multiple EG's on one box.
106     # Until that happens, we just rely on $HOME
107
108     my @bases = (
109        # '/openils/conf',     # __EG_CONFIG_DIR__
110     );
111     ($ENV{HOME}) and unshift @bases, $ENV{HOME};
112
113     return grep {-d $_} map {"$_/.ssh"} @bases;
114 }
115
116 sub local_keyfiles {
117     # populates %keyfiles hash
118     # %keyfiles maps SSH_PRIVATEKEY => SSH_PUBLICKEY
119     my $self  = shift;
120     my $force = (@_ ? shift : 0);
121     return %keyfiles if (%keyfiles and not $force);   # caching
122     $logger->info("Checking for SSH keyfiles" . ($force ? ' (ignoring cache)' : ''));
123     %keyfiles = ();  # reset to empty
124     my @dirs = plausible_dirs();
125     $logger->debug(scalar(@dirs) . " plausible dirs: " . join(', ', @dirs));
126     foreach my $dir (@dirs) {
127         foreach my $key (qw/rsa dsa/) {
128             my $private = "$dir/id_$key";
129             my $public  = "$dir/id_$key.pub";
130             unless (-r $private) {
131                 $logger->debug("Key '$private' cannot be read: $!");
132                 next;
133             }
134             unless (-r $public) {
135                 $logger->debug("Key '$public' cannot be read: $!");
136                 next;
137             }
138             $keyfiles{$private} = $public;
139         }
140     }
141     return %keyfiles;
142 }
143
144 sub param_keys {
145     my $self = shift;
146     my %keys = ();
147     if ($self->ssh_publickey and not $self->ssh_privatekey) {
148         my $private = $self->ssh_publickey;
149         unless ($private and $private =~ s/\.pub$// and -r $self->ssh_privatekey) {        # try to guess missing private key name
150             $logger->error("No ssh_privatekey specified or found to pair with " . $self->ssh_publickey);
151             return;
152         }
153         $self->ssh_privatekey($private);
154     }
155     if ($self->ssh_privatekey and not $self->ssh_publickey) {
156         my $pub = $self->ssh_privatekey . '.pub'; # guess missing public key name
157         unless (-r $pub) {
158             $logger->error("No ssh_publickey specified or found to pair with " . $self->ssh_privatekey);
159             return;
160         }
161         $self->ssh_publickey($pub);
162     }
163
164     # so now, we have either both ssh_p*keys params or neither
165     foreach (qw/ssh_publickey ssh_privatekey/) {
166         unless (-r $self->{$_}) {
167             $logger->error("$_ '" . $self->{$_} . "' cannot be read: $!");
168             return;                 # quit w/ error if we fail on any user-specified key
169         }
170     }
171     $keys{$self->ssh_privatekey} = $self->ssh_publickey;
172     return %keys;
173 }
174
175 sub new_tempfile {
176     my $self = shift;
177     my $text = shift || $self->content || ''; 
178     my $tmp  = File::Temp->new();      # magical self-destructing tempfile
179     # print $tmp "THIS IS TEXT\n";
180     print $tmp $text  or  $logger->error($self->_error("could not write to tempfile '$tmp'"));
181     close $tmp;
182     $self->tempfile($tmp);             # save the object
183     $self->local_file($tmp->filename);  # save the filename
184     $logger->info(_pkg("using tempfile $tmp"));
185     return $self->local_file;           # return the filename
186 }
187
188 sub outbound_file {
189     my $self   = shift;
190     my $params = shift;
191
192     unless (defined $self->content or $self->local_file) {   # content can be emptystring
193         $logger->error($self->_error("No content or local_file specified -- nothing to send"));
194         return;
195     }
196
197     # tricky subtlety: we want to use the most recently specified options 
198     #   with priority order: filename, content, old filename, old content.
199     # 
200     # The $params->{x} will already match $self->x after the secondary init,
201     # so the checks using $params below are for whether the value was specified NOW (e.g. via put()) or not.
202     # 
203     # if we got a new local_file value, we use it
204     # else if the content is new to this call, build a new tempfile w/ it,
205     # else use existing local_file,
206     # else build new tempfile w/ content already specified via new()
207
208     return $params->{local_file} || (
209         (defined $params->{content})          ?
210          $self->new_tempfile($self->content)  :     # $self->content is same value as $params->{content}
211         ($self->local_file || $self->new_tempfile($self->content))
212     );
213 }
214
215 sub key_check {
216     my $self   = shift;
217     my $params = shift;
218
219     return if ($params->{type} and $params->{type} eq 'FTP');   # Forget it, user specified regular FTP
220     return if (   $self->type  and    $self->type  eq 'FTP');   # Forget it, user specified regular FTP
221
222     if ($self->ssh_publickey || $self->ssh_privatekey) {
223         $self->specific(1);
224         return $self->param_keys();  # we got one or both params, but they didn't pan out
225     }
226     return local_keyfiles();     # optional "force" arg could be used here to empty cache
227 }
228
229
230 # TOP LEVEL methods
231 # TODO: delete for both uFTP and SSH2
232 # TODO: handle IO::Scalar and IO::File for uFTP
233
234 sub get {
235     my $self   = shift;
236     my $params = shift;
237     if (! ref $params) {
238         $params = {remote_file => $params} ;
239     }
240
241     $self->init($params);   # secondary init
242
243     $self->{get_args} = [$self->remote_file];      # same for scp_put and uFTP put
244     push @{$self->{get_args}}, $self->local_file if defined $self->local_file;
245     
246     # $self->content($content);
247
248     my %keys = $self->key_check($params);
249     if (%keys) {
250         my $try = $self->get_ssh2(\%keys, @{$self->{get_args}});
251         return $try if $try;  # if we had keys and they worked, we're done
252     }
253
254     # Otherwise, try w/ non-key uFTP methods
255     return $self->get_uftp(@{$self->{get_args}});
256 }
257
258 sub put {
259     my $self   = shift;
260     my $params = shift;
261     if (! ref $params) {
262         $params = {local_file => $params} ;
263     }
264
265     $self->init($params);   # secondary init
266    
267     my $local_file = $self->outbound_file($params) or return;
268
269     $self->{put_args} = [$local_file];      # same for scp_put and uFTP put
270     if (defined $self->remote_path and not defined $self->remote_file) {
271         my $rpath = $self->remote_path;
272         my $fname = basename($local_file);
273         if ($rpath =~ /^(.*)\*+(.*)$/) {    # if the path has an asterisk in it, like './incoming/*.tst'
274             my $head = $1;
275             my $tail = $2;
276             if ($tail =~ /\//) {
277                 $logger->warn($self->_error("remote path '$rpath' has dir slashes AFTER an asterisk.  Cannot determine target dir"));
278                 return;
279             }
280             if ($self->single_ext) {
281                 $tail =~ /\./ and $fname =~ s/\./_/g;    # if dot in tail, replace dots in fname (w/ _)
282             }
283             $self->remote_file($head . $fname . $tail);
284         } else {
285             $self->remote_file($rpath . '/' . $fname);   # if we know just the dir
286         }
287     }
288
289     if (defined $self->remote_file) {
290         push @{$self->{put_args}}, $self->remote_file;   # user can specify remote_file name, optionally
291     }
292
293     my %keys = $self->key_check($params);
294     if (%keys) {
295         $self->put_ssh2(\%keys, @{$self->{put_args}}) and return $self->remote_file;
296         # if we had keys and they worked, we're done
297     }
298
299     # Otherwise, try w/ non-key uFTP methods
300     return $self->put_uftp(@{$self->{put_args}});
301 }
302
303 sub ls {
304     my $self   = shift;
305     my $params = shift;
306     my @targets = @_;
307     if (! ref $params) {
308         unshift @targets, ($params || '.');   # If it was just a string, it's the first target, else default pwd
309         delete $self->{remote_file}; # overriding any target in the object previously.
310         $params = {};                # make params a normal hashref again
311     } else {
312         if ($params->{remote_file} and @_) {
313             $logger->warn($self->_error("Ignoring ls parameter remote_file for subsequent args"));
314             delete $params->{remote_file};
315         }
316         $self->init($params);   # secondary init
317         $self->remote_file and (! @targets) and push @targets, $self->remote_file;  # if remote_file is there, and there's nothing else, use it
318         delete $self->{remote_file};
319     }
320
321     $self->{ls_args} = \@targets;
322
323     my %keys = $self->key_check($params);
324     if (%keys) {
325         # $logger->info("*** calling ls_ssh2(keys, '" . join("', '", (scalar(@targets) ? map {defined $_ ? $_ : '' } @targets : ())) . "') with ssh keys");
326         my @try = $self->ls_ssh2(\%keys, @targets);
327         return @try if @try;  # if we had keys and they worked, we're done
328     }
329
330     # Otherwise, try w/ non-key uFTP methods
331     return $self->ls_uftp(@targets);
332 }
333
334 # Checks if the filename part of a pathname has one or more glob characters
335 # We split out the filename portion of the path
336 # Detect glob or no glob.
337 # returns: directory, regex for matching filenames
338 sub glob_parse {
339     my $self = shift;
340     my $path = shift or return;
341     my ($vol, $dir, $file) = File::Spec->splitpath($path); # we don't care about attempted globs in mid-filepath
342     my $front = $vol ? File::Spec->catdir($vol, $dir) : $dir;
343     $file =~ /\*/ and return ($front, glob_to_regex($file));
344     $file =~ /\?/ and return ($front, glob_to_regex($file));
345     $logger->debug("No glob detected in '$path'");
346     return;
347 }
348
349
350 # Internal Mechanics
351
352 sub _ssh2 {
353     my $self = shift;
354     $self->{ssh2} and return $self->{ssh2};     # caching
355     my $keys = shift;
356
357     my $ssh2 = Net::SSH2->new();
358     unless($ssh2->connect($self->remote_host)) {
359         $logger->warn($self->error("SSH2 connect FAILED: $! " . join(" ", $ssh2->error)));
360         return;     # we cannot connect
361     }
362
363     my $success  = 0;
364     my @privates = keys %$keys;
365     my $count    = scalar @privates;
366     foreach (@privates) {
367         if ($self->auth_ssh2($ssh2, $self->auth_ssh2_args($_, $keys->{$_}))) {
368             $success++;
369             last;
370         }
371     }
372     unless ($success) {
373         $logger->error($self->error("All ($count) keypair(s) FAILED for " . $self->remote_host));
374         return;
375     }
376     return $self->{ssh2} = $ssh2;
377 }
378
379 sub auth_ssh2 {
380     my $self = shift;
381     my $ssh2 = shift;
382     my %auth_args = @_;
383     $ssh2 or return;
384
385     my $host = $auth_args{hostname}   || 'UNKNOWN';
386     my $key  = $auth_args{privatekey} || 'UNKNOWN';
387     my $msg  = "ssh2->auth by keypair for $host using $key"; 
388     if ($ssh2->auth(%auth_args)) {
389         $logger->info("Successful $msg");
390          return 1;
391     }
392
393     if ($self->specific) {
394         $logger->error($self->error("Aborting. FAILED $msg: " . ($ssh2->error || '')));
395     } else {
396         $logger->warn($self->error("Unsuccessful keypair: FAILED $msg: " . ($ssh2->error || '')));
397     }
398     return;
399 }
400
401 sub auth_ssh2_args {
402     my $self = shift;
403     my %auth_args = (
404         privatekey => shift,
405         publickey  => shift,
406         rank => [qw/ publickey hostbased password /],
407     );
408     $self->remote_user     and $auth_args{username} = $self->remote_user    ;
409     $self->remote_password and $auth_args{password} = $self->remote_password;
410     $self->remote_host     and $auth_args{hostname} = $self->remote_host    ;
411     return %auth_args;
412 }
413
414 sub put_ssh2 {
415     my $self = shift;
416     my $keys = shift;    # could have many keypairs here
417     unless (@_) {
418         $logger->error($self->_error("put_ssh2 called without target: nothing to put!"));
419         return;
420     }
421     
422     $logger->info("*** attempting put (" . join(", ", @_) . ") with ssh keys");
423     my $ssh2 = $self->_ssh2($keys) or return;
424     my $res;
425     if ($res = $ssh2->scp_put( @_ )) {
426         $logger->info(_pkg("successfully sent", $self->remote_host, join(' --> ', @_ )));
427         return $res;
428     }
429     $logger->error($self->_error(sprintf "put with keys to %s failed with error: $!", $self->remote_host));
430     return;
431 }
432
433 sub get_ssh2 {
434     my $self = shift;
435     my $keys = shift;    # could have many keypairs here
436     unless (@_) {
437         $logger->error($self->_error("get_ssh2 called without target: nothing to get!"));
438         return;
439     }
440     
441     $logger->info("*** get args: " . Dumper(\@_));
442     $logger->info("*** attempting get (" . join(", ", map {$_ =~ /\S/ ? $_ : '*Object'} map {defined($_) ? $_ : '*Object'} @_) . ") with ssh keys");
443     my $ssh2 = $self->_ssh2($keys) or return;
444     my $res;
445     if ($res = $ssh2->scp_get( @_ )) {
446         $logger->info(_pkg("successfully got", $self->remote_host, join(' --> ', @_ )));
447         return $res;
448     }
449     $logger->error($self->_error(sprintf "get with keys from %s failed with error: $!", $self->remote_host));
450     return;
451 }
452
453 sub ls_ssh2 {
454     my $self = shift;
455     my @list = $self->ls_ssh2_full(@_);
456     @list and return sort map {$_->{slash_path}} @list;
457 #   @list and return sort grep {$_->{name} !~ /./ and {$_->{name} !~ /./ } map {$_->{slash_path}} @list;
458 }
459
460 sub ls_ssh2_full {
461     my $self = shift;
462     my $keys = shift;    # could have many keypairs here
463     my @targets = grep {defined} @_;
464
465     $logger->info("*** attempting ls ('" . join("', '", @targets) . "') with ssh keys");
466     my $ssh2 = $self->_ssh2($keys) or return;
467     my $sftp = $ssh2->sftp         or return;
468
469     my @list = ();
470     foreach my $target (@targets) {
471         my ($dir, $file);
472         my ($dirpath, $regex) = $self->glob_parse($target);
473         $dir = $sftp->opendir($dirpath || $target);     # Try to open it like a directory
474         unless ($dir) {
475             $file = $sftp->stat($target);   # Otherwise, check it like a file
476             if ($file) {
477                 $file->{slash_path} = $self->_slash_path($target, $file->{name});     # it was a file, not a dir.  That's OK.
478                 push @list, $file;
479             } else {
480                 $logger->warn($self->_error("sftp->opendir($target) failed: " . $sftp->error));
481             }
482             next;
483         }
484         my @pool = ();
485         while ($file = $dir->read()) {
486             $file->{slash_path} = $self->_slash_path($target, $file->{name});
487             push @pool, $file;
488         }
489         if ($regex) {
490             my $count = scalar(@pool);
491             @pool = grep {$_->{name} =~ /$regex/} @pool;
492             $logger->info("SSH ls: Glob regex($regex) matches " . scalar(@pool) . " of $count files"); 
493         } # else { $logger->info("SSH ls: No Glob regex in '$target'.  Just a regular ls"); }
494         push @list, @pool;
495     }
496     return @list;
497
498 }
499
500 sub _slash_path {
501     my $self = shift;
502     my $dir  = shift || '.';
503     my $file = shift || '';
504     my ($dirpath, $regex) = $self->glob_parse($dir);
505     $dir = $dirpath if $dirpath;
506     return $dir . ($dir =~ /\/$/ ? '' : '/') . $file;
507 }
508
509 sub _uftp {
510     my $self = shift;
511     my %options = ();
512     $self->{uftp} and return $self->{uftp};     # caching
513     foreach (qw/debug type port/) {
514         $options{$_} = $self->{$_} if $self->{$_};
515     }
516     
517     my $ftp = Net::uFTP->new($self->remote_host, %options);
518     unless ($ftp) {
519         $logger->error($self->_error('Net::uFTP->new("' . $self->remote_host . ", ...) FAILED: $@"));
520         return;
521     }
522
523     my @login_args = ();
524     foreach (qw/remote_user remote_password remote_account/) {
525         $self->{$_} or last;
526         push @login_args, $self->{$_};
527     }
528     my $login_ok = 0;
529     eval { $login_ok = $ftp->login(@login_args) };
530     if ($@ or !$login_ok) {
531         $logger->error($self->_error("failed login to", $self->remote_host,  "w/ args(" . join(',', @login_args) . ") : $@"));
532         return;
533     }
534     return $self->{uftp} = $ftp;
535 }
536
537 sub put_uftp {
538     my $self = shift;
539     my $ftp = $self->_uftp or return;
540     my $filename;
541     eval { $filename = $ftp->put(@{$self->{put_args}}) };
542     if ($@ or ! $filename) {
543         $logger->error($self->_error("put to", $self->remote_host, "failed with error: $@"));
544         return;
545     }
546     $self->remote_file($filename);
547     $logger->info(_pkg("successfully sent", $self->remote_host, $self->local_file, '-->', $filename));
548     return $filename;
549 }
550
551 sub get_uftp {
552     my $self = shift;
553     my $ftp = $self->_uftp or return;
554     my $filename;
555     eval { $filename = $ftp->get(@{$self->{get_args}}) };
556     if ($@ or ! $filename) {
557         $logger->error($self->_error("get from", $self->remote_host, "failed with error: $@"));
558         return;
559     }
560     $self->local_file($filename);
561     $logger->info(_pkg("successfully retrieved $filename <--", $self->remote_host . '/' . $self->remote_file));
562     return $self->local_file;
563 }
564
565 sub ls_uftp {   # returns full path like: dir/path/file.ext
566     my $self = shift;
567     my $ftp = $self->_uftp or return;
568     my @list;
569     foreach (@_) {
570         my @part;
571         my ($dirpath, $regex) = $self->glob_parse($_);
572         my $dirtarget = $dirpath || $_;
573         $dirtarget =~ s/\/+$//;
574         eval { @part = $ftp->ls($dirtarget) };      # this ls returns relative/path/filenames.  defer filename glob filtering for below.
575         if ($@) {
576             $logger->error($self->_error("ls from",  $self->remote_host, "failed with error: $@"));
577             next;
578         }
579         if ($dirtarget and $dirtarget ne '.' and $dirtarget ne './' and $ftp->is_dir($dirtarget)) {
580             foreach my $file (@part) {   # we ensure full(er) path
581                 $file =~ /^$dirtarget\// and next;
582                 $logger->debug("ls_uftp: prepending $dirtarget/ to $file");
583                 $file = File::Spec->catdir($dirtarget, $file);
584             }
585         }
586         if ($regex) {
587             my $count = scalar(@part);
588             # @part = grep {my @a = split('/',$_); scalar(@a) ? /$regex/ : ($a[-1] =~ /$regex/)} @part;
589             my @bulk = @part;
590             @part = grep {
591                         my ($vol, $dir, $file) = File::Spec->splitpath($_);
592                         $file =~ /$regex/
593                     } @part;  
594             $logger->info("FTP ls: Glob regex($regex) matches " . scalar(@part) . " of $count files");
595         } #  else {$logger->info("FTP ls: No Glob regex in '$_'.  Just a regular ls");}
596         push @list, @part;
597     }
598     return @list;
599 }
600
601 sub delete_uftp {
602     my $self = shift;
603     my $ftp = $self->_uftp or return;
604     return $ftp->delete(shift);
605 }
606
607 sub _pkg {      # Not OO
608     return __PACKAGE__ . ' : ' unless @_;
609     return __PACKAGE__ . ' : ' . join(' ', @_);
610 }
611
612 sub _error {
613     my $self = shift;
614     return _pkg($self->error(join(' ',@_)));
615 }
616
617 sub init {
618     my $self   = shift;
619     my $params = shift;
620     my @required = @_;  # qw(remote_host) ;     # nothing required now
621
622     if ($params->{account_object}) {    # if we got passed an object, we initialize off that first
623         $self->{remote_host    } = $params->{account_object}->host;
624         $self->{remote_user    } = $params->{account_object}->username;
625         $self->{remote_password} = $params->{account_object}->password;
626         $self->{remote_account } = $params->{account_object}->account;
627         $self->{remote_path    } = $params->{account_object}->path;     # not really the same as remote_file, maybe expand on this later
628     }
629
630     foreach (keys %{$self->{_permitted}}) {
631         $self->{$_} = $params->{$_} if defined $params->{$_};   # possibly override settings from object
632     }
633
634     foreach (@required) {
635         unless ($self->{$_}) {
636             $logger->error("Required parameter $_ not specified");
637             return;
638         }
639     }
640     return $self;
641 }
642
643 sub new {
644     my ($class, %args) = @_;
645     my $self = { _permitted => \%fields, %fields };
646
647         bless $self, $class;
648
649     $self->init(\%args); # or croak "Initialization error caused by bad args";
650     return $self;
651 }
652
653 sub DESTROY { 
654         # in order to create, we must first ...
655         my $self  = shift;
656     $self->{ssh2} and $self->{ssh2}->disconnect();  # let the other end know we're done.
657     $self->{uftp} and $self->{uftp}->quit();  # let the other end know we're done.
658 }
659
660 sub AUTOLOAD {
661         my $self  = shift;
662         my $class = ref($self) or croak "AUTOLOAD error: $self is not an object";
663         my $name  = $AUTOLOAD;
664
665         $name =~ s/.*://;   #   strip leading package stuff
666
667         unless (exists $self->{_permitted}->{$name}) {
668                 croak "AUTOLOAD error: Cannot access '$name' field of class '$class'";
669         }
670
671         if (@_) {
672                 return $self->{$name} = shift;
673         } else {
674                 return $self->{$name};
675         }
676 }
677
678 1;
679