Chapter 8

Reading and Writing Files from an Applet

by Mark Wutka


CONTENTS

Applets and Files

Because of security restrictions, an applet may not read or write files on the local system. This is to protect you from a malicious applet storing a virus on your computer or deleting all your files. Even if you could only read files on the local system, someone could snoop around on your computer and retrieve private information.

Before you start exploring the various methods of storing files from an applet, ask yourself first whether you want your applet manipulating files directly. If you are designing a system where you have an applet presenting the user interface for an application running on a server, you should consider doing all the file access from the application. The more code you put on the applet, the longer it takes for it to start up. Even if you have designed your application this way, you may still have reasons to directly read and write files from the applet. For example, you might want to save configuration information for the user interface (colors, fonts). It wouldn't make sense to make this part of the main application, because separating the user interface from the application also means separating the application from the user interface. In other words, the application shouldn't know anything about the user interface, except how it interacts with the applet.

Using the JFS Filesystem for Applets

The JFS filesystem, by Jamie Cameron, is one of the most useful sets of Java classes to come along. It provides NFS-like file and print services for applets. You can get this wonderful product on the Internet at http://www.ncs.com.sg/java/jfs.

JFS solves a number of pesky problems for applets. It provides a way to read and write files, create and delete files and directories, print files, and open URLs and socket connections to hosts other than the ones the applets came from. JFS isn't some sort of cheap hack around Java's security model; it is a full-featured server system. This means, of course, that you have to run the JFS server, which is written in Java, to use JFS.

The JFSclient class is the applet's interface to the JFS server. You create an instance of JFSclient by passing the host name of the JFS server to the constructor. If you are doing this from an applet, the host must be the host that the applet was loaded from, which means your Web server has to run JFS.

Once you create a JFSclient, you must send a user name and password to the JFS server. JFS has its own set of user names and passwords; these are not the operating system's user names. This is quite important because the applet must contain the user name and password in order to perform the logon. Anyone with evil intentions and a little patience can find out the user name and password that the applet sends. If these were logon IDs for your Web server, it would be simple for someone to log on to your Web server and wreak all kinds of havoc. If you forget to send the authentication information, the other methods in the JFSclient will simply hang, which may not be quite the result you were looking for.

Listing 8.1 shows a very simple example that retrieves a file stored in the JFS file system.


Listing 8.1  Source Code for JFSGet.java
import java.applet.*;

// This program demonstrates the use of the JFSclient
// class to fetch a file.

public class JFSGet extends Object
{
     public static void main(String args[])
     {
          try {

// Create a JFS client to host 192.0.0.3
               JFSclient jfs = new JFSclient("192.0.0.3");
          
// Log on as root, with no password
               jfs.auth("root", "");

// Fetch the file called "volcano"
               byte[] volcfile = jfs.get("/home/root/volcano", 0);

// Dump it to the screen
               for (int i=0; i < volcfile.length; i++) {
                    System.out.print((char)volcfile[i]);
               }
               System.out.println();

          } catch (Exception e) {
               e.printStackTrace();
          }
     }
}

The JFSGet program is very straightforward. It creates a JFSclient object that is connected to a JFS server whose IP address is 192.0.0.3, then it authenticates itself using the name root with no password. Next, it uses the get method to retrieve a file as an array of bytes. Finally, it loops through the array of bytes and prints them to the System.out.

Caution
This example does not use an authentication password. In practice, you should always use a password in JFS authentication. Otherwise, you may open your system up to possible corruption from other people on the Internet.

Printing Files Using JFS

Under most of the current browser implementations, you may not print Java applets within a Web page. You will usually find an unfortunate blank space on your printout where the applet should be. There aren't even any methods or classes within Java to perform printing functions. While JFS can't quite deal with rendering the output from your applet on a printer, it does allow you to send text data to a printer. If you have a PostScript printer, you could sent PostScript codes to the printer and get graphical output. Printer access in JFS is done through a special device called "/dev/Printer." This one device file represents all the printers on your system. You choose the printer when you send a message to this device. Unlike UNIX, in which you use the same functions to write to devices or files, JFS uses special methods named devput and devget to communicate with the special devices. When you want to send data to a printer, you can pass the printer name to the device driver using the devput method. The following code fragment sends an array of bytes to a printer named myprinter:

JFSclient myClient = new JFSClient(getDocumentBase().getHost());
byte databytes[] =  (some way of getting data bytes)
Message deviceInfo = new Message();
deviceInfo.add("Printer", "myprinter");
myClient.devput("/dev/Printer", databytes, deviceInfo);

The printers are defined in a file called /etc/printers within the JFS filesystem. If you are running UNIX, you don't need to be concerned about all these /dev and /etc files. JFS maintains its own filesystem structure, the root of which can be in any directory on the system. What JFS calls /etc/printers may really be /home/mark/jfs/root/etc/printers. The /etc/printers file contains multiple lines that each contain four fields separated by colons:

printer name:printer type:printer description:print command

The default /etc/printers that comes with JFS contains the single line:

default:Postscript:The Default Printer:lpr

Accessing Other Web Servers from JFS

One of the other problems that JFS solves for applets is the nasty restriction of not being able to access any other servers other than the one the applet came from. The /dev/Web device in JFS is a URL redirection device that enables you to retrieve data from any URL on the Net. Use the devget method to tell the /dev/Web device where to get the data from:

JFSclient myClient = new JFSClient(getDocumentBase().getHost());
Message deviceInfo = new Message();
deviceInfo.add("URL", "http://www.mcp.com");
Message response = myClient.devget("/dev/Web", deviceInfo);
byte[] responseData = response.getdata();

Saving Files Using HTTP Post

If you don't want the overhead of JFS, you have some alternatives. You can take advantage of the existing classes that are able to post data to a Web server. Remember that when you post data to a Web server, you are essentially sending it a file. The only thing you have to do is create something to take the posted data and store it in a file. This turns out to be a trivial task under UNIX. The following shell script enables you to store data in a file:

#!/bin/sh
echo "Content-type: text/plain"
echo
dd ibs=1 count=$CONTENT_LENGTH of=$QUERY_STRING 2>/dev/null
echo $?

This script probably requires a bit of explanation. First of all, the filename that you are storing into is encoded in the URL and not in the posted data. If you were to call this script putfile and put it in the cgi-bin directory of your Web server, the following URL would try to post to a file called putme:

http://mywebhost/cgi-bin/putfile?putme

The shell script kicks into action by first printing response data that is required by every CGI program, namely the "Context-type" information. Next, it prints a blank line, signifying the end of the header information. Now comes the key to this script-the dd command. This command is similar to the cat command, except that it has the ability to read and write a fixed number of records. The ibs=1 option tells dd to use an input block size of 1 byte, meaning that the count option will tell how many bytes to read. Because the http server stores the number of bytes you posted in an environment variable called CONTENT_LENGTH, you use this variable as the count parameter for dd. Finally, the of parameter is the name of the output file you are writing to. Any error messages are sent to /dev/null, but the numeric exit code is printed as the final line of the response generated by the script. If the dd command is successful, the echo $? line will echo a 0.

Listing 8.2 shows a class that uses the POST command to send data to this script.


Listing 8.2  Source Code for PostPutFile.java
import java.net.*;
import java.io.*;

// This class provides a static method to post a file to the
// putfile script, which takes a filename as a parameter passed
// in the POST request itself, and then receives the bytes as
// the posted data.

public class PostPutFile extends Object
{
// Put sends the named file to a specific URL. The URL should
// contain the path name of the putfile script. This method
// will append the ?filename to the script name.
// It returns 0 if the put was successful, or a non-zero number
// if it failed for some reason.

    public static int put(URL url, String filename, byte[] bytes)
    throws IOException, MalformedURLException
    {
// Run the putfile script and ask it to store the data in a file called "putme"

        URL destURL = new URL(url.getProtocol(), url.getHost(),
          url.getPort(), url.getFile()+"?"+filename);

// Define the data that you want stored in the file.

        URLConnection urlConn = destURL.openConnection();

        urlConn.setDoOutput(true);    // we need to write
        urlConn.setDoInput(true);    // just to be safe...
        urlConn.setUseCaches(false);    // get info fresh from server

// Tell the server what kind of data we are sending - in this case,
// just a stream of bytes.

        urlConn.setRequestProperty("Content-type",
            "application/octet-stream");

// Must tell the server the size of the data we are sending. This also
// tells the URLConnection class that we are doing a POST instead
// of a GET.

        urlConn.setRequestProperty("Content-length", ""+bytes.length);

// Open an output stream so we can send the info we are posting

        OutputStream outStream = urlConn.getOutputStream();

// Write out the actual request data

        outStream.write(bytes);

        outStream.close();

// Now that we have sent the data, open up an input stream and get
// the response back from the server

        DataInputStream inStream = new DataInputStream(
            urlConn.getInputStream());

        String line = inStream.readLine();

        inStream.close();

        try {
            int result = Integer.valueOf(line).intValue();
            return result;
        } catch (Exception parseError) {
            return -1;
        }
    }
}

Listing 8.3 shows a simple example applet that stores a file using the PostPutFile class.


Listing 8.3  Source Code for TestPutFile.java
import java.net.*;
import java.applet.*;

public class TestPutFile extends Applet
{
     public void init()
     {
          try {
               URL destURL = new URL(getDocumentBase(),
                    "/cgi-bin/putfile");

// Define a string we want to send

               String dataToSend = "This is a string that I want \n"+
                    "to store in the file.\n";

// The PostPutFile class wants a byte array, however, so we convert
// the string to a byte array.

               byte[] bytes = new byte[dataToSend.length()];

               dataToSend.getBytes(0, dataToSend.length(), bytes, 0);

               PostPutFile.put(destURL, "/home/mark/putme", bytes);

          } catch (Exception e) {
               e.printStackTrace();
          }
     }
}

Note
The new version of HTTP (HTTP 1.1) includes a PUT command that allows you to store a file without creating a separate CGI program to save the file. Some HTTP servers already support this new option. If you have a server that supports PUT and you want to save files from Java, you won't be able to use the URL class to send the file (until the URL class supports POST). You can, however, use the PostSockURL class from Chapter 6 "Communicating with a Web Server," with a little modification (change POST to PUT when it sends the HTTP command).

Storing and Retrieving Files with FTP

As its name implies, the File Transfer Protocol is useful for sending files back and forth between an applet and a server. You just need to make sure that your server supports FTP. If you are running UNIX or Windows NT, it should come with the operating system. The FTP protocol is defined in Internet RFC 959.

The FTP protocol requires you to use two different connections between client and server. The control connection is used by the client to send commands to the server. The server sends responses over the control connection. Whenever the client or server needs to send a large block of data, a data connection must be established. Figure 8.1 illustrates the connections between a client and an FTP server.

Figure 8.1 : A client uses two different connections to communicate with an FTP server.

Sending FTP Commands

The control connection used in FTP is an ASCII, line-oriented connection, similar to SMTP (Simple Mail Transfer Protocol). A client sends a command to the server as a line terminated by carriage. The response from the server is one or more ASCII lines containing a 3-digit return code and a text response. For example, when you send the USER command to tell the FTP server that you want to log in as mark, you send:

USER mark

The FTP server would respond with a line like:

331 Password required for mark

The response codes from FTP are grouped into five categories, based on the first digit in the response code:

FTP responses can span more than one line. Whenever the server sends a multiline response, each line begins with the response code followed by a dash (-). The last line of the response does not contain a dash. All you have to do when reading responses is look for a dash as the fourth character. If there is a dash, you need to read another line. Listing 8.4 shows the doCommand and getResponse methods from the FTPSession class which are included on the CD for this book. These methods are responsible for sending commands and receiving responses. The getResponse method checks for a dash to see if the response is a multiline response. You could use these same methods for other Internet protocols that use this same request-response format, like SMTP.

Note
The FTPSession class uses DataInputStream and DataOutputStream filters on top of the normal socket input and output streams. This allows FTPSession to send and receive whole lines of data rather than reading and writing one character at a time.


Listing 8.4  doCommand and getResponse Methods from FTPSession.java
// Send a command and wait for a response

     public String doCommand(String commandString)
     throws IOException
     {
          outStream.writeBytes(commandString+"\n");
          String response = getResponse();
          return response;
     }

// Get a response back from the server. Handles multi-line responses
// and returns them as part of the string.

     public String getResponse()
     throws IOException
     {
          String response = "";

          for (;;) {
               String line = inStream.readLine();

               if (line == null) {
                    throw new IOException(
                         "Bad response from server.");
               }

// FTP response lines should at the very least have a 3-digit number

               if (line.length() < 3) {
                    throw new IOException(
                         "Bad response from server.");
               }
               response += line + "\n";

// If there isn't a '-' immediately after the number, we've gotten the
// complete response. ('-' is the continuation character for FTP responses)

               if ((line.length() == 3) ||
                    (line.charAt(3) != '-')) return response;
          }
     }

Tip
If you already had a class that implemented the SMTP protocol, you might consider moving the methods for sending and receiving commands into a new superclass for the SMTP class. Then the FTP and SMTP classes would be subclasses of this new class. This kind of situation occurs often in object-oriented programming. You discover that there are parts of a class that can be used by other classes, so you split out the reusable parts into a separate class. Obviously, it would have been better if you could have anticipated that the parts would need to be reused, but you don't always realize these things ahead of time.

Establishing an FTP Session

The first step in establishing an FTP session is creating the connection to the FTP server. FTP connections are normal TCP socket connections. The default FTP port is 21, but you should allow for other port numbers, since you may be running a special version of FTP for another reason.

Once you have established a connection, the FTP server will send you a response, which is really just a greeting from the server. It is very important that you be prepared to read this response once you have connected to the server. If you don't read the response and just go on and send a USER command to begin your login, you will be confused and think that the greeting is the response from the USER command, and you will probably remain confused for the rest of the session. The greeting usually looks something like this:

220 flamingo FTP server (Version wu-2.4(1) Wed May 10 21:00:32 CDT 1995) ready

Once you connect to the server, you have to log on. At the minimum, you must send a USER command. This command is in the format:

USER username

For example, for you to sign on as mark, you would use the command line:

USER mark

Since most user names on an FTP server have a password (if they don't, there's a security risk), the server will most likely respond with a command like:

331 Password required for mark

You would then be required to send a password with the PASS command:

PASS password

If mark's password is Shh!!!!!, the appropriate PASS command is:

PASS Shh!!!!!

The response to the PASS command is usually something like this:

230 User mark logged in.

The FTP protocol allows for a third login parameter called the account, which is sent using the ACCT command. If you get a response with a response code of 332 (need account for login) after sending the PASS command, you need to send an ACCT command:

ACCT account

The account parameter is rarely used on UNIX systems, and is not restricted to the login sequence. You could receive a 332 response code for any operation, meaning that you must supply an account parameter when performing that operation. For instance, your server may password-protect files, and could require you to send the password to a file with the ACCT command before you can retrieve the file.

Sending Simple FTP Commands

The FTP protocol supports many useful commands that allow you to perform most important file operations. Some of the simple FTP commands are shown in Table 8.1.

Table 8.1  Some Common FTP Commands

CommandFunction
CWD directory Changes the working directory
CDUP Changes directory to the parent of the current directory (like cd .. in UNIX or Windows)
DELE filename Deletes a file
MKD directory Creates a new directory
RMD directory Deletes a directory
RNFR old-filename Renames old-filename (must be followed by RNTO, which gives the new name)
RNTO new-filename Sets the new name of a file being renamed by a RNFR command

A simple command is one that does not require a data connection. Some FTP commands require you to set up a second connection, either to send raw data to the server, or receive raw data from the server. Table 8.2 shows you the commands that require a data connection.

Table 8.2  FTP Commands that Require a Data Connection

CommandFunction
LIST Gets a list of all the files in the current directory
LIST directory Gets a list of all the files in a specific directory
RETR filename Retrieves a file from the FTP server
STOR filename Sends a file to the FTP server

Establishing a Data Connection

Whenever an FTP server needs to transfer raw data to or from a client, it uses a separate data connection for the transfer. Normally, the client sets up a listen socket (in Java, a ServerSocket) to accept an incoming connection from the FTP server. The client then sends the host address and port number of the listen socket to the server using the PORT command. The format of the PORT command is:

PORT h1,h2,h3,h4,p1,p2

The h1-h4 parameters are the individual bytes in the client's host address. If the client's host address was 192.0.0.1, the bytes would be 192,0,0,1. The p1 and p2 parameters are the upper and lower bytes of the listen socket's port number. The following code fragment extracts p1 and p2 from a port number:

int p1 = (portNumber >> 8) & 0xff;
int p2 = portNumber & 0xff;

An example PORT command for a host with an address is 192.0.0.3 and a port number of 1234 is:

PORT 192,0,0,3,4,210

You can verify that the port bytes of 4 and 210 are indeed port 1234 by computing (p1*256) + p2, or (4*256) + 210.

Once you send the PORT command, you can issue a command that requires a data connection like RETR, STOR, or LIST. Once you issue a command that requires a data connection, the server will connect to the listen socket and either send or receive data, depending on the command. Figure 8.2 shows the typical interaction sequence between an FTP server and a client performing a RETR command.

Figure 8.2 : A server connects to a client to establish a data connection.

The normal method of data connection will not work for Java applet because an applet may not accept an incoming socket connection. Fortunately, the FTP protocol gives you another option for establishing a data connection-the PASV command.

The PASV command is similar to the PORT command, except that it causes the server to create the listen socket. The response from the PASV command gives the host address and port number for the listen socket in the same h1,h2,h3,h4,p1,p2 format as used in the PORT command. Here is an example PASV command and the response from the server:

PASV
227 Entering Passive Mode (127,0,0,1,6,114)

Once the server returns the response, the client can establish the data connection. Figure 8.3 shows the typical interaction sequence between an FTP server and a client performing a STOR command, using PASV to set up the data connection.

Figure 8.3 : The PASV command forces the server to create the listen socket for the data connection.

Listing 8.5 shows the doPasvPort method from the FTPSession class. It sends a PASV command, parses the response, and then establishes a socket connection with the server.


Listing 8.5  doPasvPort Method from FTPSession.java
protected synchronized Socket doPasvPort()
     throws IOException
     {

// Send the PASV command
          String response = doCommand("PASV");

// If it wasn't in the 200s, there was an error

          if (response.charAt(0) != '2') {
               throw new IOException(response);
          }

// The pasv response looks like: 
// 227 Entering Passive Mode (127,0,0,1,4,160)
// We'll look for the ()'s at the end first

          int parenStart = response.lastIndexOf('(');
          int parenEnd = response.lastIndexOf(')');

// Make sure they're both there and that the ) comes after the (
          if ((parenStart < 0) || (parenEnd < 0) ||
               (parenStart >= parenEnd)) {
               throw new IOException("PASV response format error");
          }

// Extract the address bytes
          String pasvAddr = response.substring(parenStart+1, parenEnd);

// Create a tokenizer to parse the bytes
          StringTokenizer tokenizer = new StringTokenizer(pasvAddr, ",");

// Create the array to store the bytes
          int[] addrValues = new int[6];

// Parse each byte
          for (int i=0; (i < 6) && tokenizer.hasMoreTokens(); i++) {
               try {
                    addrValues[i] = Integer.valueOf(
                         tokenizer.nextToken()).intValue();
               } catch (Exception e) {
                    throw new IOException(
                         "PASV response format error");
               }
          }

// We ignore the host addresses, assuming that the host address is
// the same as the host address we used to connect the first time.

          Socket newSock = new Socket(host, (addrValues[4] << 8) +
               addrValues[5]);

          return newSock;
     }

Listing 8.6 shows the put method from the FTPSession class. It uses the doPasvPort command to set up a data connection, then sends a STOR command to the FTP server. The STOR command should return a response code in the 100-199 range, indicating that the STOR may proceed. When you finish sending the file to the FTP server, you must close down the data connection. This tells the server that you have finished. You should then receive another response from the server over the command connection, which should have a response code in the 200-299 range.


Listing 8.6  put Method from FTPSession.java
public synchronized void put(String remoteFile, byte[] data,
          boolean doBinary)
     throws IOException
     {

// If transferring in binary mode, send a type command for type I (IMAGE)
          if (doBinary) {
               String response = doCommand("TYPE I");
               if (response.charAt(0) != '2') {
                    throw new IOException(response);
               }

// If transferring in ASCII mode, send a type command for type A (ASCII)
          } else {
               String response = doCommand("TYPE A");
               if (response.charAt(0) != '2') {
                    throw new IOException(response);
               }
          }

// Open up a data connection
          Socket putSock = doPasvPort();

// Tell the server where we want it to store the data we are sending

          String response = doCommand("STOR "+remoteFile);

// If the request is successful, the server should send a response
// in the 100s and then start receiving the bytes. Once the data
// connection is closed, it should send a response in the 200s.

          if (response.charAt(0) != '1') {
               putSock.close();
               throw new IOException(response);
          }

// If binary mode, just write all the bytes
          if (doBinary) {
               OutputStream out = putSock.getOutputStream();

               out.write(data);

// If ASCII mode, write the data a line at a time

          } else {
               DataInputStream in = new DataInputStream(
                    new ByteArrayInputStream(data));
               DataOutputStream out = new DataOutputStream(
                    putSock.getOutputStream());

               String line;

               while ((line = in.readLine()) != null) {
                    out.writeBytes(line+"\r");
               }
          }

          putSock.close();

          response = getResponse();

// Make sure we got a 200 response

          if (response.charAt(0) != '2') {
               throw new IOException(response);
          }
     }

The FTPSession class is quite simple to use. You just create an instance of FTPSession by passing the destination host name, the user name, and the password to the constructor, and then using the get and put methods to retrieve and send files, respectively. Listing 8.7 shows an example applet that copies a file by retrieving it and then storing it under a new name.


Listing 8.7  Source Code for TryFTP.java
import java.applet.*;
import java.io.*;

// This applet demonstrates the use of the FTPSession class.
// It copies a file called "volcano" to a file called "vol.ftp"
// by fetching the file and then storing it with a new name.

public class TryFTP extends Applet
{
     public void init()
     {
          try {
// Create the session to host 192.0.0.3, using a user name of anonymous
// and a password of mark@localhost

               FTPSession sess = new FTPSession(
                    "192.0.0.3",
                    "anonymous", "mark@localhost");

// Fetch the file
               byte[] file = sess.get("/home/mark/volcano", true);

// Store the file
               sess.put("/home/mark/vol.ftp", file, true);
               

          } catch (Exception e) {
               e.printStackTrace();
          }
     }
}

Caution
Be extremely careful when using the FTPSession class with respect to the user name and password. Even though your applet is compiled, it is fairly trivial to look through the code and find the user name and password that are sent. You should either use anonymous FTP, or set up a user account that is not allowed to log on to your system and is allowed to use only FTP. Otherwise, you are broadcasting a free user account all over the Internet whenever you put your applet out there.