The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Net::FTPServer - A secure, extensible and configurable Perl FTP server

SYNOPSIS

  ftpd [--help] [-d] [-v] [-p port] [-s] [-S] [-V] [-C conf_file] [-P pidfile]

DESCRIPTION

Net::FTPServer is a secure, extensible and configurable FTP server written in Perl.

Current features include:

 * Authenticated FTP access.
 * Anonymous FTP access.
 * Complete implementation of current RFCs.
 * ASCII or binary type file transfers.
 * Active or passive mode file transfers.
 * Run standalone or from inetd(8).
 * Security features: chroot, resource limits, tainting,
   protection against buffer overflows.
 * IP-based and/or IP-less virtual hosts.
 * Complete access control system.
 * Anonymous read-only FTP personality.
 * Virtual filesystem allows files to be served
   from a database.
 * Directory aliases and CDPATH support.
 * Extensible command set.

INSTALLING AND RUNNING THE SERVER

A standard ftpd.conf file is supplied with the server. You should study the comments in the file and edit it to your satisfaction, and then copy it to /etc/ftpd.conf.

  cp ftpd.conf /etc/
  chown root.root /etc/ftpd.conf
  chmod 0755 /etc/ftpd.conf

Two start-up scripts are supplied with the ftp server, to run it in two common configurations: either as a full FTP server or as an anonymous-only read-only FTP server. The scripts are ftpd and ro-ftpd. You may need to edit these scripts if Perl is not stored in the standard place on your system (the default path is /usr/bin/perl).

You should copy the appropriate script, either ftpd or ro-ftpd to a suitable place (for example: /usr/sbin/in.ftpd).

  cp ftpd /usr/sbin/in.ftpd
  chown root.root /usr/sbin/in.ftpd
  chmod 0755 /usr/sbin/in.ftpd

STANDALONE SERVER

If you have a high load site, you will want to run Net::FTPServer as a standalone server. To start Net::FTPServer as a standalone server, do:

  /usr/sbin/in.ftpd -S

You may want to add this to your local start-up files so that the server starts automatically when you boot the machine.

To stop the server, do:

  killall in.ftpd

RUNNING FROM INETD

Add the following line to /etc/inetd.conf:

  ftp stream tcp nowait root /usr/sbin/tcpd in.ftpd

(This assumes that you have the tcpd package installed to provide basic access control through /etc/hosts.allow and /etc/hosts.deny. This access control is in addition to any access control which you may configure through /etc/ftpd.conf.)

After editing this file you will need to inform inetd:

  killall -HUP inetd

COMMAND LINE FLAGS

  --help       Display help and exit
  -d, -v       Enable debugging
  -p PORT      Listen on port PORT instead of the default port
  -s           Run in daemon mode (default: run from inetd)
  -S           Run in background and in daemon mode
  -V           Show version information and exit
  -C CONF      Use CONF as configuration file (default: /etc/ftpd.conf)
  -P PIDFILE   Save pid into PIDFILE (daemon mode only)
  --test       Test mode (used only in automatic testing scripts)

CONFIGURING AND EXTENDING THE SERVER

Net::FTPServer can be configured and extended in a number of different ways.

Firstly, almost all common server configuration can be carried out by editing the configuration file /etc/ftpd.conf.

Secondly, commands can be loaded into the server at run-time to provide custom extensions to the common FTP command set. These custom commands are written in Perl.

Thirdly, one of several different supplied personalities can be chosen. Personalities can be used to make deep changes to the FTP server: for example, there is a supplied personality which allows the FTP server to serve files from a relational database. By subclassing Net::FTPServer, Net::FTPServer::DirHandle and Net::FTPServer::FileHandle you may also write your own personalities.

The next sections talk about each of these possibilities in turn.

EDITING /etc/ftpd.conf

A standard /etc/ftpd.conf file is supplied with Net::FTPServer in the distribution. This contains all possible configurable options, information about them and defaults. You should consult the comments in this file for authoritative information.

LOADING CUSTOMIZED SITE COMMANDS

It is very simple to write custom SITE commands. These commands are available to users when they type "SITE XYZ" in a command line FTP client or when they define a custom SITE command in their graphical FTP client.

SITE commands are unregulated by RFCs. You may define any commands and give them any names and any function you wish. However, over time various standard SITE commands have been recognized and implemented in many FTP servers. Net::FTPServer also implements these. They are:

  SITE VERSION      Display the server software version.
  SITE EXEC         Execute a shell command on the server (in
                    C<Net::FTPServer> this is disabled by default!)
  SITE ALIAS        Display chdir aliases.
  SITE CDPATH       Display chdir paths.
  SITE CHECKMETHOD  Implement checksums.
  SITE CHECKSUM
  SITE IDLE         Get or set the idle timeout.

The following commands are found in wu-ftpd, but not currently implemented by Net::FTPServer: SITE CHMOD, SITE GPASS, SITE GROUP, SITE GROUPS, SITE INDEX, SITE MINFO, SITE NEWER, SITE UMASK.

So when you are choosing a name for a SITE command, it is probably best not to choose one of the above names, unless you are specifically implementing or overriding that command.

Custom SITE commands have to be written in Perl. However, there is very little you need to understand in order to write these commands -- you will only need a basic knowledge of Perl scripting.

As our first example, we will implement a SITE README command. This command just prints out some standard information.

Firstly create a file called /usr/local/lib/site_readme.pl (you may choose a different path if you want). The file should contain:

  sub {
    my $self = shift;
    my $cmd = shift;
    my $rest = shift;

    $self->reply (200,
                  "This is the README file for mysite.example.com.",
                  "Mirrors are contained in /pub/mirrors directory.",
                  "       :       :       :       :       :",
                  "End of the README file.");
  }

Edit /etc/ftpd.conf and add the following command:

site command: readme /usr/local/lib/site_readme.pl

and restart the FTP server (check your system log [/var/log/messages] for any syntax errors or other problems). Here is an example of a user running the SITE README command:

  ftp> quote help site
  214-The following commands are recognized:
  214-    ALIAS   CHECKMETHOD     EXEC    README
  214-    CDPATH  CHECKSUM        IDLE    VERSION
  214 You can also use HELP to list general commands.
  ftp> site readme
  200-This is the README file for mysite.example.com.
  200-Mirrors are contained in /pub/mirrors directory.
  200-       :       :       :       :       :
  200 End of the README file.

Our second example demonstrates how to use parameters (the $rest argument). This is the SITE ECHO command.

  sub {
    my $self = shift;
    my $cmd = shift;
    my $rest = shift;

    # Split the parameters up.
    my @params = split /\s+/, $rest;

    # Quote each parameter.
    my $reply = join ", ", map { "'$_'" } @params;

    $self->reply (200, "You said: $reply");
  }

Here is the SITE ECHO command in use:

  ftp> quote help site
  214-The following commands are recognized:
  214-    ALIAS   CHECKMETHOD     ECHO    IDLE
  214-    CDPATH  CHECKSUM        EXEC    VERSION
  214 You can also use HELP to list general commands.
  ftp> site echo hello how are you?
  200 You said: 'hello', 'how', 'are', 'you?'

Our third example is more complex and shows how to interact with the virtual filesystem (VFS). The SITE SHOW command will be used to list text files directly (the user normally has to download the file and view it locally). Hence SITE SHOW readme.txt should print the contents of the readme.txt file in the local directory (if it exists).

All file accesses must be done through the VFS, not by directly accessing the disk. If you follow this convention then your commands will be secure and will work correctly with different back-end personalities (in particular when ``files'' are really blobs in a relational database).

  sub {
    my $self = shift;
    my $cmd = shift;
    my $rest = shift;

    # Get the file handle.
    my ($dirh, $fileh, $filename) = $self->_get ($rest);

    # File doesn't exist or not accessible. Return an error.
    unless ($fileh)
      {
        $self->reply (550, "File or directory not found.");
        return;
      }

    # Check it's a simple file.
    my ($mode) = $fileh->status;

    unless ($mode eq "f")
      {
        $self->reply (550,
                      "SITE SHOW command is only supported on plain files.");
        return;
      }

    # Try to open the file.
    my $file = $fileh->open ("r");

    unless ($file)
      {
        $self->reply (550, "File or directory not found.");
        return;
      }

    # Copy data into memory.
    my @lines = ();

    while ($_ = $file->getline)
      {
        # Remove any native line endings.
        s/[\n\r]+$//;

        push @lines, $_;
      }

    # Close the file handle.
    $file->close;

    # Send the file back to the user.
    $self->reply (200, "File $filename:", @lines, "End of file.");
  }

This code is not quite complete. A better implementation would also check the "retrieve rule" (so that people couldn't use SITE SHOW in order to get around access control limitations which the server administrator has put in place). It would also check the file more closely to make sure it was a text file and would refuse to list very large files.

Here is an example (abbreviated) of a user using the SITE SHOW command:

  ftp> site show README
  200-File README:
  200-$Id: FTPServer.pm,v 1.33 2001/02/22 15:46:12 rich Exp $
  200-
  200-Net::FTPServer - A secure, extensible and configurable Perl FTP server.
  [...]
  200-To contact the author, please email: Richard Jones <rich@annexia.org>
  200 End of file.

STANDARD PERSONALITIES

Currently Net::FTPServer is supplied with three standard personalities. These are:

  Full    The complete read/write anonymous/authenticated FTP
          server which serves files from a standard Unix filesystem.

  RO      A small read-only anonymous-only FTP server similar
          in functionality to Dan Bernstein's publicfile
          program.

  DBeg1   An example FTP server which serves files to a PostgreSQL
          database. This supports files and hierarchical
          directories, multiple users (but not file permissions)
          and file upload.

The standard Full personality will not be explained here.

The RO personality is the Full personality with all code related to writing files, creating directories, deleting, etc. removed. The RO personality also only permits anonymous logins and does not contain any code to do ordinary authentication. It is therefore safe to use the RO personality where you are only interested in serving files to anonymous users and do not want to worry about crackers discovering a way to trick the FTP server into writing over a file.

The DBeg1 personality is a complete read/write FTP server which stores files as BLOBs (Binary Large OBjects) in a PostgreSQL relational database. The personality supports file download and upload and contains code to authenticate users against a users table in the database (database ``users'' are thus completely unrelated to real Unix users). The DBeg1 is intended only as an example. It does not support advanced features such as file permissions and quotas. As part of the schoolmaster.net project Bibliotech Ltd. have developed an even more advanced database personality which supports users, groups, access control lists, quotas, recursive moves and copies and many other features. However this database personality is not available as source.

To use the DBeg1 personality you must first run a PostgreSQL server (version 6.4 or above) and ensure that you have access to it from your local user account. Use the initdb, createdb and createuser commands to create the appropriate user account and database (please consult the PostgreSQL administrators manual for further information about this -- I do not answer questions about basic PostgreSQL knowledge).

Here is my correctly set up PostgreSQL server, accessed from my local user account ``rich'':

  cruiser:~$ psql
  Welcome to the POSTGRESQL interactive sql monitor:
    Please read the file COPYRIGHT for copyright terms of POSTGRESQL

     type \? for help on slash commands
     type \q to quit
     type \g or terminate with semicolon to execute query
   You are currently connected to the database: rich

  rich=> \d
  Couldn't find any tables, sequences or indices!

You will also need the following Perl modules installed: DBI, DBD::Pg.

Now you will need to create a database called ``ftp'' and populate it with data. This is how to do this:

  createdb ftp
  psql ftp < doc/eg1.sql

Check that no ERRORs are reported by PostgreSQL.

You should now be able to start the FTP server by running the following command (not as root):

  ./dbeg1-ftpd -S -p 2000 -C ftpd.conf

If the FTP server doesn't start correctly, you should check the system log file [/var/log/messages].

Connect to the FTP server as follows:

  ftp localhost 2000

Log in as either rich/123456 or dan/123456 and then try to move around, upload and download files, create and delete directories, etc.

SUBCLASSING THE Net::FTPServer CLASSES

By subclassing Net::FTPServer, Net::FTPServer::DirHandle and/or Net::FTPServer::FileHandle you can create custom personalities for the FTP server.

Typically by overriding the hooks in the Net::FTPServer class you can change the basic behaviour of the FTP server - turning it into an anonymous read-only server, for example.

By overriding the hooks in Net::FTPServer::DirHandle and Net::FTPServer::FileHandle you can create virtual filesystems: serving files into and out of a database, for example.

The current manual page contains information about the hooks in Net::FTPServer which may be overridden.

See Net::FTPServer::DirHandle(3) for information about the methods in Net::FTPServer::DirHandle which may be overridden.

See Net::FTPServer::FileHandle(3) for information about the methods in Net::FTPServer::FileHandle which may be overridden.

The most reasonable way to create your own personality is to extend one of the existing personalities. Choose the one which most closely matches the personality that you want to create. For example, suppose that you want to create another database personality. A good place to start would be by copying lib/Net/FTPServer/DBeg1/*.pm to a new directory lib/Net/FTPServer/MyDB/ (for example). Now edit these files and substitute "MyDB" for "DBeg1". Then examine each subroutine in these files and modify them, consulting the appropriate manual page if you need to.

VIRTUAL HOSTS

Net:FTPServer is capable of hosting multiple FTP sites on a single machine. Because of the nature of the FTP protocol, virtual hosting is almost always done by allocating a single separate IP address per FTP site. However, Net::FTPServer also supports an experimental IP-less virtual hosting system, although this requires modifications to the client.

Normal (IP-based) virtual hosting is carried out as follows:

 * For each FTP site, allocate a separate IP address.
 * Configure IP aliasing on your normal interface so that
   the single physical interface responds to multiple
   virtual IP addresses.
 * Add entries (A records) in DNS mapping each site's
   name to a separate IP address.
 * Add reverse entries (PTR records) in DNS mapping each
   IP address back to the site hostname. It is important
   that both forward and reverse DNS is set up correctly,
   else virtual hosting may not work.
 * In /etc/ftpd.conf you will need to add a virtual host
   section for each site like this:

     <Host sitename>

       ip: 1.2.3.4
       ... any specific configuration options for this site ...

     </Host>

   You don't in fact need the "ip:" part assuming that
   your forward and reverse DNS are set up correctly.
 * If you want to specify a lot of external sites, or
   generate the configuration file automatically from a
   database or a script, you may find the <Include filename>
   syntax useful.

There are examples in /etc/ftpd.conf. Here is how IP-based virtual hosting works:

 * The server starts by listening on all interfaces.
 * A connection arrives at one of the IP addresses and a
   process is forked off.
 * The child process finds out which interface the
   client connected to and reverses the name.
 * If:
     the IP address matches one of the "ip:" declarations
     in any of the "Host" sections, 
   or:
     there is a reversal for the name, and the name
     matches one of the "Host" sections in the configuration
     file,
   then:
     configuration options are read from that
     section of the file and override any global configuration
     options specified elsewhere in the file.
 * Otherwise, the global configuration options only
   are used.

IP-less virtual hosting is an experimental feature. It requires the client to send a HOST command very early on in the command stream -- before USER and PASS. The HOST command explicitly gives the hostname that the FTP client is attempting to connect to, and so allows many FTP sites to be multiplexed onto a single IP address. At the present time, I am not aware of any FTP clients which implement the HOST command, although they will undoubtedly become more common in future.

This is how to set up IP-less virtual hosting:

 * Add entries (A or CNAME records) in DNS mapping the
   name of each site to a single IP address.
 * In /etc/ftpd.conf you will need to list the same single
   IP address to which all your sites map:

     virtual host multiplex: 1.2.3.4

 * In /etc/ftpd.conf you will need to add a virtual host
   section for each site like this:

     <Host sitename>

       ... any specific configuration options for this site ...

     </Host>

Here is how IP-less virtual hosting works:

 * The server starts by listening on one interface.
 * A connection arrives at the IP address and a
   process is forked off.
 * The IP address matches "virtual host multiplex"
   and so no IP-based virtual host processing is done.
 * One of the first commands that the client sends is
   "HOST" followed by the hostname of the site.
 * If there is a matching "Host" section in the
   configuration file, then configuration options are
   read from that section of the file and override any
   global configuration options specified elsewhere in
   the file.
 * If there is no matching "Host" section then the
   global configuration options alone are used.

The client is not permitted to issue the HOST command more than once, and is not permitted to issue it after login.

VIRTUAL HOSTING AND SECURITY

Only certain configuration options are available inside the <Host> sections of the configuration file. Generally speaking, the only configuration options you can put here are ones which take effect after the site name has been determined -- hence "allow anonymous" is OK (since it's an option which is parsed after determining the site name and during log in), but "port" is not (since it is parsed long before any clients ever connect).

Make sure your default global configuration is secure. If you are using IP-less virtual hosting, this is particularly important, since if the client never sends a HOST command, the client gets the global configuration. Even with IP-based virtual hosting it may be possible for clients to sometimes get the global configuration, for example if your local name server fails.

IP-based virtual hosting always takes precedence above IP-less virtual hosting.

With IP-less virtual hosting, access control cannot be performed on a per-site basis. This is because the client has to issue commands (ie. the HOST command at least) before the site name is known to the server. However you may still have a global "access control rule".

METHODS

    Net::FTPServer->run ([\@ARGV]);

    This is the main entry point into the FTP server. It starts the FTP server running. This function never normally returns.

    If no arguments are given, then command line arguments are taken from the global @ARGV array.

    $ftps->reply ($code, $line, [$line, ...])

    This function sends a standard single line or multi-line FTP server reply to the client. The $code should be one of the standard reply codes listed in RFC 959. The one or more $line arguments are the (free text) of the reply. Do not include carriage returns at the end of each $line. This function adds the correct line ending format as specified in the RFC.

    $ftps->config ($name);

    Read configuration option $name from the configuration file.

    $ftps->ip_host_config ($ip_addr);

    Look for a <Host> section which contains "ip: $ip_addr". If one is found, return the site name of the Host section. Otherwise return undef.

    $sock = $self->open_data_connection;

    Open a data connection. Returns the socket (an instance of IO::Socket) or undef if it fails for some reason.

    $self->pre_configuration_hook ();

    Hook: Called before command line arguments and configuration file are read.

    Status: optional.

    Notes: You may append your own information to $self-{version_string}> from this hook.

    $self->options_hook (\@args);

    Hook: Called before command line arguments are parsed.

    Status: optional.

    Notes: You can use this hook to supply your own command line arguments. If you parse any arguments, you should remove them from the @args array.

    $self->post_configuration_hook ();

    Hook: Called after all command line arguments and configuration file have been read and parsed.

    Status: optional.

    $self->post_bind_hook ();

    Hook: Called only in daemon mode after the control port is bound but before starting the accept infinite loop block.

    Status: optional.

    $self->pre_accept_hook ();

    Hook: Called in daemon mode only just before accept(2) is called in the parent FTP server process.

    Status: optional.

    $self->post_accept_hook ();

    Hook: Called both in daemon mode and in inetd mode just after the connection has been accepted. This is called in the child process.

    Status: optional.

    $rv = $self->access_control_hook;

    Hook: Called after accept(2)-ing the connection to perform access control. Detailed request information is contained in the $self object. If the function returns -1 then the socket is immediately closed and no FTP processing happens on it. If the function returns 0, then normal access control is performed on the socket before FTP processing starts. If the function returns 1, then normal access control is not performed on the socket and FTP processing begins immediately.

    Status: optional.

    $rv = $self->process_limits_hook;

    Hook: Called after accept(2)-ing the connection to perform per-process limits (eg. by using the setrlimit(2) system call). Access control has already been performed and detailed request information is contained in the $self object.

    If the function returns -1 then the socket is immediately closed and no FTP processing happens on it. If the function returns 0, then normal per-process limits are applied before any FTP processing starts. If the function returns 1, then normal per-process limits are not performed and FTP processing begins immediately.

    Status: optional.

    $rv = $self->authentication_hook ($user, $pass, $user_is_anon)

    Hook: Called to perform authentication. If the authentication succeeds, this should return 0. If the authentication fails, this should return -1.

    Status: required.

    $self->user_login_hook ($user, $user_is_anon)

    Hook: Called just after user $user has successfully logged in. A good place to change uid and chroot if necessary.

    Status: optional.

    $dirh = $self->root_directory_hook;

    Hook: Return an instance of a subclass of Net::FTPServer::DirHandle corresponding to the root directory.

    Status: required.

    $self->pre_command_hook;

    Hook: This hook is called just before the server begins to wait for the client to issue the next command over the control connection.

    Status: optional.

    $rv = $self->command_filter_hook ($cmdline);

    Hook: This hook is called immediately after the client issues command $cmdline, but before any checking or processing is performed on the command. If this function returns -1, then the server immediately goes back to waiting for the next command. If this function returns 0, then normal command filtering is carried out and the command is processed. If this function returns 1 then normal command filtering is not performed and the command processing begins immediately.

    Important Note: This hook must be careful not to overwrite the global $_ variable.

    Do not use this function to add your own commands. Instead use the $self->{command_table} and $self->{site_command_table} hashes.

    Status: optional.

    $error = $self->transfer_hook ($mode, $file, $sock, \$buffer);

      $mode     -  Open mode on the File object (Either reading or writing)
      $file     -  File object as returned from DirHandle::open
      $sock     -  Data IO::Socket object used for transfering
      \$buffer  -  Reference to current buffer about to be written

    The \$buffer is passed by reference to minimize the stack overhead for efficiency purposes only. It is not meant to be modified by the transfer_hook subroutine. (It can cause corruption if the length of $buffer is modified.)

    Hook: This hook is called after reading $buffer and before writing $buffer to its destination. If arg1 is "r", $buffer was read from the File object and written to the Data socket. If arg1 is "w", $buffer will be written to the File object because it was read from the Data Socket. The return value is the error for not being able to perform the write. Return undef to avoid aborting the transfer process.

    Status: optional.

    $self->post_command_hook

    Hook: This hook is called after all command processing has been carried out on this command.

    Status: optional.

BUGS

The SIZE, REST and RETR commands probably do not work correctly in ASCII mode.

REST does not work before STOR/STOU/APPE (is it supposed to?)

You cannot abort a transfer in progress yet. Nor can you check the status of a transfer in progress. Using the telnet interrupt commands can cause the FTP server to fail.

User upload/download limits.

Limit number of clients. Limit number of clients by host or IP address.

The following commands are recognized by wu-ftpd, but are not yet implemented by Net::FTPServer:

  SITE CHMOD   There is a problem supporting this with our VFS.
  SITE GPASS   Group functions are not really relevant for us.
  SITE GROUP   -"- ditto -"-
  SITE GROUPS  -"- ditto -"-
  SITE INDEX   This is a synonym for SITE EXEC.
  SITE MINFO   This command is no longer supported by wu-ftpd.
  SITE NEWER   This command is no longer supported by wu-ftpd.
  SITE UMASK   This command is difficult to support with VFS.

Symbolic links are not handled elegantly (or indeed at all) yet.

The program needs to log a lot more general transfer and access information to syslog.

Equivalent of ProFTPD's ``DisplayReadme'' function.

The ability to hide dot files (probably best to build this into the VFS layer). This should apply across all commands. See ProFTPD's ``IgnoreHidden'' function.

Do ident (RFC913) authentication at login. Have a way to turn this on and off.

Access to LDAP authentication database (can currently be done using a PAM module). In general, we should support pluggable authentication.

Log formatting similar to ProFTPD command LogFormat.

More timeouts to avoid various denial of service attacks. For example, the server should always timeout when waiting too long for an active data connection.

Support for IPv6 (see RFC 2428), EPRT, EPSV commands.

Upload and download tar.gz/zip files automatically.

See also "XXX" comments in the code for other problems, missing features and bugs.

FILES

  /etc/ftpd.conf
  /usr/lib/perl5/site_perl/5.005/Net/FTPServer.pm
  /usr/lib/perl5/site_perl/5.005/Net/FTPServer/DirHandle.pm
  /usr/lib/perl5/site_perl/5.005/Net/FTPServer/FileHandle.pm
  /usr/lib/perl5/site_perl/5.005/Net/FTPServer/Handle.pm

AUTHORS

Richard Jones (rich@annexia.org).

COPYRIGHT

Copyright (C) 2000 Biblio@Tech Ltd., Unit 2-3, 50 Carnwath Road, London, SW6 3EG, UK

SEE ALSO

Net::FTPServer::Handle(3), Net::FTPServer::FileHandle(3), Net::FTPServer::DirHandle(3), Authen::PAM(3), Net::FTP(3), perl(1), RFC 765, RFC 959, RFC 1579, RFC 2389, RFC 2428, RFC 2577, RFC 2640, Extensions to FTP Internet Draft draft-ietf-ftpext-mlst-NN.txt.

2 POD Errors

The following errors were encountered while parsing the POD:

Around line 609:

You can't have =items (as at line 687) unless the first thing after the =over is an =item

Around line 4794:

=back doesn't take any parameters, but you said =back 4