### ### Copyright 2000-2007 University of Illinois Board of Trustees ### All rights reserved. ### ### AnonFTP.pm - anonymous FTP module for psgconf ### ### Campus Information Technologies and Educational Services ### University of Illinois at Urbana-Champaign ### package PSGConf::Control::AnonFTP; use strict; use File::stat; use Unix::Mknod qw(major minor); use PSGConf::Action::MkDir; use PSGConf::Action::MkNod; use PSGConf::Action::Symlink; use PSGConf::Action::CopyFile; use PSGConf::Action::CreateFile; use PSGConf::Action::GenerateFile::EnvFile; use PSGConf::Action::GenerateFile::Literal; use PSGConf::Action::GenerateFile::etc_passwd; use PSGConf::Action::GenerateFile::ftpaccess; use PSGConf::Action::Remove; use PSGConf::Action::svcs::setprop; use PSGConf::Data::Boolean; use PSGConf::Data::Hash; use PSGConf::Data::List; use PSGConf::Data::String; use PSGConf::Control::PAM qw(_add_pam); use PSGConf::Control::Packages qw(_add_pkgs); use PSGConf::Control::syslog qw(_add_syslog); ############################################################################### ### policy methods ############################################################################### ### ftp server name should default to hostname sub _policy_default_servername { my ($self, $psgconf) = @_; my ($tmp); return if ($psgconf->data_obj('anon_ftp_enable')->equals('false')); $psgconf->data_obj('anon_ftp_server_name')->set( $psgconf->data_obj('hostname')->get() ) if (!defined $psgconf->data_obj('anon_ftp_server_name')->get()); $tmp = 'ftpadmin@' . $psgconf->data_obj('anon_ftp_server_name')->get(); $psgconf->data_obj('anon_ftp_options')->insert( { 'email' => $tmp } ) if (!defined $psgconf->data_obj('anon_ftp_options')->find('email')); } ### uploads should be disabled by default sub _policy_default_uploads { my ($self, $psgconf) = @_; my ($tmp); return if ($psgconf->data_obj('anon_ftp_enable')->equals('false')); ### no uploading allowed by default $tmp = $psgconf->data_obj('anon_ftp_dir')->get() . ' *'; $psgconf->data_obj('anon_ftp_upload')->insert( { $tmp => 'no' } ) if (!defined $psgconf->data_obj('anon_ftp_upload')->find($tmp)); } ### create ftp user sub _policy_add_user { my ($self, $psgconf) = @_; my ($user, $group); return if ($psgconf->data_obj('anon_ftp_enable')->equals('false')); $user = $psgconf->data_obj('anon_ftp_user')->get(); $group = $psgconf->data_obj('anon_ftp_group')->get(); $psgconf->data_obj('group_info')->insert( { $group => {} } ) if (!defined $psgconf->data_obj('group_info')->find($group)); $psgconf->data_obj('user_info')->insert( { $user => { group => $group, passwd => $psgconf->data_obj('passwd_token')->get(), home => $psgconf->data_obj('anon_ftp_dir')->get(), gecos => 'Anonymous FTP', shell => '/bin/false', home_mode => 0555 } } ) if (!defined $psgconf->data_obj('user_info')->find($user)); } ### create ftp aliases sub _policy_add_sendmail_aliases { my ($self, $psgconf) = @_; my ($user); return if ($psgconf->data_obj('anon_ftp_enable')->equals('false')); $user = $psgconf->data_obj('anon_ftp_user')->get(); $psgconf->data_obj('sendmail_aliases')->insert( { $user => 'root' } ) if (!defined $psgconf->data_obj('sendmail_aliases')->find($user)); $psgconf->data_obj('sendmail_aliases')->insert( { 'ftpadmin' => $user } ) if (!$psgconf->data_obj('sendmail_aliases')->exists('ftpadmin')); } ### add PAM configs sub _policy_add_pam { my ($self, $psgconf) = @_; my ($service, $lines); $service = ( $psgconf->data_obj('platform')->match('linux')) ? 'ftp' : 'ftpd'; $self->{pam_name} = $service; $self->{pam_conf} = $psgconf->data_obj('anon_ftp_pam_conf')->get(); $self->_add_pam($psgconf) if ($psgconf->data_obj('anon_ftp_pam_conf')->count()); } ### add syslog entry sub _policy_add_syslog { my ($self, $psgconf) = @_; $self->_add_syslog($psgconf) if (exists($self->{syslog})); } ### add inetd entry sub _policy_add_inetd_entry { my ($self, $psgconf) = @_; return if ($psgconf->data_obj('anon_ftp_enable')->equals('false')); $psgconf->data_obj('inetd')->insert( { 'ftp/tcp' => { 'server' => $psgconf->data_obj('anon_ftp_ftpd_path')->get() } } ) if ( ! $psgconf->data_obj('platform')->match('solaris10') ); $psgconf->data_obj('inetd')->insert( { 'ftp/tcp' => { 'server_args' => '-a -d -l -r ' . $psgconf->data_obj('anon_ftp_dir')->get() } } ) if ( $psgconf->data_obj('anon_ftp_use_vsftpd')->equals('false') && ! $psgconf->data_obj('platform')->match('solaris10') ); } sub _enable_rc_scripts { my ($self, $psgconf) = @_; $psgconf->data_obj('rc_scripts')->insert( { 'ftp' => { 'state' => 'enable' }} ) if ( $psgconf->data_obj('anon_ftp_enable')->equals('true') && defined $psgconf->data_obj('rc_scripts')->find('ftp')); } ### add TCP wrappers entry sub _policy_add_tcpwrapper_entry { my ($self, $psgconf) = @_; my ($cmd); return if ($psgconf->data_obj('anon_ftp_enable')->equals('false')); $cmd = ($psgconf->data_obj('anon_ftp_use_vsftpd')->equals('true')) ? 'vsftpd' : 'in.ftpd'; $psgconf->data_obj('tcp_wrappers')->insert_row( { 1 => 'all' }, [ $cmd, 'all', 'allow' ] ) if (! $psgconf->data_obj('tcp_wrappers')->find_row( { 0 => qr/\b$cmd\b/ })); } ############################################################################### ### decide() method ############################################################################### sub decide { my ($self, $psgconf) = @_; my ($file, $dest, $mode, $uid, $gid, $conversions, $userinfo, $user); $mode = PSGConf::Data::Integer->new(); ### if we're not enabled, remove ftpaccess and return if ($psgconf->data_obj('anon_ftp_enable')->equals('false')) { $psgconf->register_actions( PSGConf::Action::Remove->new( name => $psgconf->data_obj('anon_ftp_cfg_dir')->get() . '/ftpaccess' ) ); return; } ### add fake /etc/passwd file with just the anon ftp account. $user = $psgconf->data_obj('anon_ftp_user')->get(); %{$userinfo->{'ftp'}} = %{$psgconf->data_obj('user_info')->find($user)}; $userinfo->{'ftp'}->{home} = '/'; map { $conversions .= $_ . ":" . $psgconf->data_obj('anon_ftp_conversions')->find($_) . "\n" } keys %{$psgconf->data_obj('anon_ftp_conversions')->get()}; $uid = ( defined $psgconf->data_obj('user_info')->find($psgconf->data_obj('anon_ftp_user')->get()))? $psgconf->data_obj('user_info')->find($psgconf->data_obj('anon_ftp_user')->get())->{'uid'}: -1; $gid = ( defined $psgconf->data_obj('user_info')->find($psgconf->data_obj('anon_ftp_user')->get()))? $psgconf->data_obj('user_info')->find($psgconf->data_obj('anon_ftp_user')->get())->{'gid'}: -1; $psgconf->register_actions( (map { $file = $psgconf->data_obj('anon_ftp_chroot_files')->find($_); if ( exists $file->{location} ) { $dest = $psgconf->data_obj('anon_ftp_dir')->get() . $file->{location}; } else { $dest = $psgconf->data_obj('anon_ftp_dir')->get() . $_; } $uid = ( exists $file->{owner} )? (( defined $psgconf->data_obj('user_info')->find($file->{owner}) )? $psgconf->data_obj('user_info')->find($file->{owner})->{uid}: 0): 0; $gid = ( exists $file->{group} )? (( defined $psgconf->data_obj('user_info')->find($file->{group}) )? $psgconf->data_obj('user_info')->find($file->{group})->{gid}: 0): 0; ### If the source file does not exist, assume you ### want to create a directory. if ( -d $_ || ! -e $_ ) { $mode->set((exists $file->{mode})? $file->{mode}: 0111); PSGConf::Action::MkDir->new( name => $dest, mode => $mode->get(), uid => $uid, gid => $gid ); } elsif ( -l $_ ) { PSGConf::Action::Symlink->new( name => $dest, link_to => $_ ); } elsif ( -b $_ || -c $_ ) { my ($st) = stat $_; $mode->set((exists $file->{mode})? $file->{mode}: 0644); PSGConf::Action::MkNod->new( name => $dest, mode => $mode->get(), type => (-b $_)? 'b': 'c', major => major($st->rdev), minor => minor($st->rdev) ); } elsif ( -f $_ ) { $mode->set((exists $file->{mode})? $file->{mode}: 0444); PSGConf::Action::CopyFile->new( name => $dest, copy_from => $_, mode => $mode->get(), uid => $uid, gid => $gid ); } } keys %{$psgconf->data_obj('anon_ftp_chroot_files')->get()}), PSGConf::Action::GenerateFile::etc_passwd->new( 'name' => $psgconf->data_obj('anon_ftp_dir')->get() . '/etc/passwd', 'comment_str' => undef, 'mode' => 0444, 'sort_func' => sub { $a cmp $b }, 'user_info' => $userinfo, 'passwd_token' => $psgconf->data_obj('passwd_token')->get(), 'add_passwords' => ($psgconf->data_obj('platform')->match('hpux')) ), PSGConf::Action::CreateFile->new( 'name' => $psgconf->data_obj('anon_ftp_dir')->get() . '/var/adm/wtmpx', ) ); ### Now take care of the vsftpd/wu-ftpd specific files if ( $psgconf->data_obj('anon_ftp_use_vsftpd')->equals('true') ) { $psgconf->register_actions( PSGConf::Action::GenerateFile::EnvFile->new( name => '/etc/vsftpd/vsftpd.conf', vars => $psgconf->data_obj('anon_ftp_vsftpd_options')->get() ), PSGConf::Action::GenerateFile::Literal->new( name => '/etc/vsftpd.ftpusers', content => join ("\n", @{$psgconf->data_obj('anon_ftp_users')->get()}) . "\n" ), PSGConf::Action::GenerateFile::Literal->new( name => '/etc/vsftpd.user_list', content => join ("\n", @{$psgconf->data_obj('anon_ftp_users')->get()}) . "\n" ) ); } else { $psgconf->register_actions( PSGConf::Action::GenerateFile::ftpaccess->new( name => $psgconf->data_obj('anon_ftp_cfg_dir')->get() . '/ftpaccess', description => 'FTP server configuration file', classes => $psgconf->data_obj('anon_ftp_class')->get(), autogroups => $psgconf->data_obj('anon_ftp_autogroup')->get(), limits => $psgconf->data_obj('anon_ftp_limit')->get(), options => $psgconf->data_obj('anon_ftp_options')->get(), messages => $psgconf->data_obj('anon_ftp_message')->get(), readmes => $psgconf->data_obj('anon_ftp_readme')->get(), uploads => $psgconf->data_obj('anon_ftp_upload')->get(), literal_config => $psgconf->data_obj('anon_ftp_literal')->get() ), PSGConf::Action::GenerateFile::Literal->new( name => '/etc/ftpusers', content => join ("\n", @{$psgconf->data_obj('anon_ftp_users')->get()}) . "\n" ), (map { PSGConf::Action::GenerateFile::Literal->new( name => $psgconf->data_obj('anon_ftp_dir')->get() . $psgconf->data_obj('anon_ftp_cfg_dir')->get() . '/ftp-banners/' . $_, comment_str => undef, content => $psgconf->data_obj('anon_ftp_banners')->find($_) ) } sort keys %{$psgconf->data_obj('anon_ftp_banners')->get()}), PSGConf::Action::GenerateFile::Literal->new( name => $psgconf->data_obj('anon_ftp_dir')->get() . $psgconf->data_obj('anon_ftp_cfg_dir')->get() . '/ftpconversions', content => $conversions ) ); } if ( $psgconf->data_obj('platform')->match('solaris10') ) { $psgconf->register_actions( PSGConf::Action::svcs::setprop->new( name => 'enable FTP startup options', FMRI => 'svc:/network/ftp', property => 'inetd_start/exec', value => '"' . $psgconf->data_obj('anon_ftp_ftpd_path')->get() . ' -a -d -l -r ' . $psgconf->data_obj('anon_ftp_dir')->get() . '"' ) ); } } ############################################################################### ### Constructor ############################################################################### sub new { my ($class, $psgconf) = @_; my ($self); $self = {}; bless($self, $class); ### So that _add_pkgs knows which directives to look at $self->{name} = 'anon_ftp'; $self->{enable} = $self->{name} . '_enable'; $self->{packages} = $self->{name} . '_packages'; ### So that _add_syslog knows to add syslog file support. ### If we are running on Linux or the *BSD's then use ftp facility, ### otherwise the system logs by default to DAEMON if ( $psgconf->data_obj('platform')->match('linux|bsd')) { $self->{syslog} = 'ftp'; $self->{facility} = $self->{syslog} . '.info'; } $psgconf->register_data( anon_ftp_autogroup => PSGConf::Data::Hash->new(), anon_ftp_banners => PSGConf::Data::Hash->new(), anon_ftp_conversions => PSGConf::Data::Hash->new(), anon_ftp_class => PSGConf::Data::List->new(), anon_ftp_user => PSGConf::Data::String->new( 'value' => 'ftp' ), anon_ftp_users => PSGConf::Data::List->new(), anon_ftp_group => PSGConf::Data::String->new( 'value' => 'ftp' ), anon_ftp_dir => PSGConf::Data::String->new( 'value_abspath' => 1, 'value' => '/services/ftp' ), anon_ftp_enable => PSGConf::Data::Boolean->new( 'value' => 'false' ), anon_ftp_packages => PSGConf::Data::List->new(), anon_ftp_limit => PSGConf::Data::List->new(), anon_ftp_literal => PSGConf::Data::String->new(), anon_ftp_cfg_dir => PSGConf::Data::String->new( 'value_abspath' => 1, 'value' => '/etc' ), anon_ftp_ftpd_path => PSGConf::Data::String->new( 'value_abspath' => 1, 'value' => '/usr/sbin/in.ftpd' ), anon_ftp_message => PSGConf::Data::Hash->new(), anon_ftp_options => PSGConf::Data::Hash->new(), anon_ftp_vsftpd_options => PSGConf::Data::Hash->new(), anon_ftp_chroot_files => PSGConf::Data::Hash->new( value_type => 'HASH' ), anon_ftp_readme => PSGConf::Data::Hash->new(), anon_ftp_server_name => PSGConf::Data::String->new(), anon_ftp_upload => PSGConf::Data::Hash->new( key_abspath => 1 ), anon_ftp_pam_conf => PSGConf::Data::List->new(), anon_ftp_use_vsftpd => PSGConf::Data::Boolean->new( value => 'false' ) ); $psgconf->register_policy($self, anon_ftp_default_servername => '_policy_default_servername', anon_ftp_default_uploads => '_policy_default_uploads', anon_ftp_add_user => '_policy_add_user', anon_ftp_add_sendmail_aliases => '_policy_add_sendmail_aliases', anon_ftp_add_inetd_entry => '_policy_add_inetd_entry', anon_ftp_add_tcpwrapper_entry => '_policy_add_tcpwrapper_entry', anon_ftp_add_syslog => '_policy_add_syslog', anon_ftp_add_packages => '_add_pkgs', anon_ftp_add_pam => '_policy_add_pam', anon_ftp_enable_rc_scripts => '_enable_rc_scripts' ); return $self; } ############################################################################### ### documentation ############################################################################### 1; __END__ =head1 NAME PSGConf::Control::AnonFTP - psgconf control class for anonymous FTP =head1 SYNOPSIS In F: Control PSGConf::Control::AnonFTP =head1 DESCRIPTION The B module provides a B control object for configuring anonymous FTP using wu-ftpd. It provides the following methods: =over 4 =item new() The constructor. Its parameter is a reference to the B object. It registers the following data objects: =over 4 =item I A B object containing the absolute path to the wu-ftpd binary. The default is F. =item I A B object that specifies autogroup mappings. The hash key is the group name, and the value is a space-seperated list of classes. =item I A B object that specifies what FTP banner files need to be created. The hash key is the name of the file, and the value is the file's contents. =item I A B object that contains a list of classes to define in F. =item I A B object that sets the path to the anonymous FTP root. It must be an absolute path. If not set, it defaults to F. =item I A B object that sets the login for the ftpd to run as. Defaults to C. =item I A B object that sets the group for the ftpd to run as. Defaults to C. =item I A B object that defines the contents of the ftpusers file. =item I A B object that enables anonymous FTP. Default is off. =item I A B object lists what packages to install. =item I A B object that contains a list of limits to define in F. =item I A B object containing the contents of the F file, where the key is the first 4 fields of the ftpconversions file (the Strip prefix, Strip postfix, Addon prefix, and Addon postfix) and the value is the rest of the line in the file. =item I A B object that contains the location of all the ftpd configuration files. Defaults to F. =item I A B object that contains literal text to add to F. (This is a nasty hack, and should be avoided.) =item I A B object that specifies the C directives to add to F. The hash key is the name of the message file, and the value sets when the directive is triggered. =item I A B object that contains miscellaneous options for F. The hash key is the option name, and the value is the option value. =item I A B object that contains the contents of the F file. =item I A B object listing what files are needed to setup the chroot'ed ftp environment. The key is the file on the system, and points to a hash with the following sub keys: =item I A B object to set I (provided by B), to configure PAM for ftp server support. =item I A B object to tell the control object to configure using C instead of C. Defaults to off. =over 4 =item I The location to put the file under I. =item I The mode the file should have. =back =item I A B object that specifies the C directives to add to F. The hash key is the name of the F file, and the value sets when the directive is triggered. =item I A B object that contains the advertised server name of the FTP server. If not set, it defaults to the value of the I object, which is supplied by B. =item I A B object that specifies the C directives to add to F. The hash key is a string containing the root directory, followed by a space, followed by the list of relative directories (or a C<*>). The value is C, or C followed by optional owner and permissions for the uploaded files. =back In addition, the constructor registers the following policy methods: =over 4 =item I If the I object is unset, it is set to the value of the I object, which is provided by B. If the I object does not contain a setting for the C option, it is set to C followed by the value of the I object. =item I If the I object is unset, an entry is added for root directory I, subdirectory C<*>, value C. =item I Adds entries to the I and I Data objects (supplied by the B module) for user and group C. =item I Adds entries from I to the I Data object (supplied by the B module) for the C service. =item I If mail aliases for C and C do not exist, aliases are created that point to C. This is done using the I Data object, which is provided by the B module. =item I If there is an entry in the I hash for I, then go ahead and enable it. =item I If there is no entry in the I data object (provided by B) for C, create one that invokes the binary specified by the I data object with the arguments C<-a -d -l -r anon_ftp_dir>. =item I If the I object has no entry for C, add one with the values C, C, C. This is done using the I object, which is provided by B. =item I Under Linux and *BSD, adds an F entry to the I data object (supplied by B). (On other platforms, there is no C syslog facility, so F logs to the C facility.) =item I Adds F to the list of requested packages. This uses the I config object supplied by B. =back =item decide() Instantiates and registers the following action objects: =over 4 =item * If I is not set, it registers a B object to remove F and returns immediately. =item * Instantiates a B object to create F. =item * Instantiates B objects to create each of the files listed in the I object. The files are created in the F directory. =item * Instantiates B objects to create the ftpconversions file. =item * Instantiates B objects to create the F file. =item * Instantiates B objects to create the I/etc/passwd file. =item * If we are running on Solaris10, instantiates B objects to set the SMF properties for ftpd to C<-a -d -l -r anon_ftp_dir>. =item * Instantiates B object for each file listed in I object. Instantiates B for each symlink, B for each block or character special file and B for each directory or if the file does not exist. =back =back =head1 SEE ALSO L ftpaccess(5) L L L L L L L L L L L L L L L L L L L L L L L L L L =cut