This program implements the client half of the TFTP application protocol, a well-known service that has been used in the Unix world for network booting of workstations since before Windows 3.1. I chose this protocol because it’s widely implemented on the server side, so it’s easy to find a test server for it.
The TFTP protocol is a bit odd. The client contacts the server on the well-known UDP port number 69, from a generated port number,[34] and the server responds to the client from a generated port number. Further communication is on the two generated port numbers.
Getting into more detail, as shown in Figure 15-1, the client initially sends a read request with the filename, and reads the first chunk of data. The read request consists of two bytes (a short) with the read request code (short integer with a value of 1, defined as OP_RRQ), two bytes for the sequence number, then the ASCII filename, null terminated, and the string octet, also null terminated. The server verifies that it can open the file and, if so, sends the first data packet (OP_DATA), and then reads again. This read-acknowledge cycle is repeated until all the data is read. Note that each packet is 516 bytes (512 bytes of data, plus 2 bytes for the packet type and 2 more for the packet number) except the last, which can be any length from 4 (zero bytes of data) to 515 (511 bytes of data). If there is a network I/O error, the packet is re-sent. If a given packet goes astray, both client and server are supposed to perform a timeout cycle. This client does not, but the server does. You could add timeouts using a thread; see Section 24.5. The client code is shown in Example 15-9.
Example 15-9. RemCat.java
import java.io.*; import java.net.*; /** * RemCat - remotely cat (DOS type) a file, using the TFTP protocol. * Inspired by the "rcat" exercise in Learning Tree Course 363, * <I>UNIX Network Programming</I>, by Dr. Chris Brown. * * Note that the TFTP server is NOT "internationalized"; the name and * mode in the protocol are defined in terms of ASCII, not UniCode. */ public class RemCat { /** The UDP port number */ public final static int TFTP_PORT = 69; /** The mode we will use - octet for everything. */ protected final String MODE = "octet"; /** The offset for the code/response as a byte */ protected final int OFFSET_REQUEST = 1; /** The offset for the packet number as a byte */ protected final int OFFSET_PACKETNUM = 3; /** Debugging flag */ protected static boolean debug = false; /** TFTP op-code for a read request */ public final int OP_RRQ = 1, /** TFTP op-code for a read request */ OP_WRQ = 2, /** TFTP op-code for a read request */ OP_DATA = 3, /** TFTP op-code for a read request */ OP_ACK = 4, /** TFTP op-code for a read request */ OP_ERROR = 5; protected final static int PACKET = 516; // == 2 + 2 + 512 protected String host; protected InetAddress servAddr; protected DatagramSocket sock; protected byte buffer[]; protected DatagramPacket inp, outp; /** The main program that drives this network client. * @param argv[0] hostname, running TFTP server * @param argv[1..n] filename(s), must be at least one */ public static void main(String[] argv) throws IOException { if (argv.length < 2) { System.err.println("usage: java RemCat host filename[...]"); System.exit(1); } if (debug) System.err.println("Java RemCat starting"); RemCat rc = new RemCat(argv[0]); for (int i = 1; i<argv.length; i++) { if (debug) System.err.println("-- Starting file " + argv[0] + ":" + argv[i] + "---"); rc.readFile(argv[i]); } } RemCat(String host) throws IOException { super( ); this.host = host; servAddr = InetAddress.getByName(host); sock = new DatagramSocket( ); buffer = new byte[PACKET]; inp = new DatagramPacket(buffer, PACKET); outp = new DatagramPacket(buffer, PACKET, servAddr, TFTP_PORT); } void readFile(String path) throws IOException { /* Build a tftp Read Request packet. This is messy because the * fields have variable length. Numbers must be in * network order, too; fortunately Java just seems * naturally smart enough :-) to use network byte order. */ buffer[0] = 0; buffer[OFFSET_REQUEST] = OP_RRQ; // read request int p = 2; // number of chars into buffer // Convert filename String to bytes in buffer , using "p" as an // offset indicator to get all the bits of this request // in exactly the right spot. path.getBytes(0, path.length( ), buffer, p); // file name p += path.length( ); buffer[p++] = 0; // null byte terminates string // Similarly, convert MODE ("octet") to bytes in buffer MODE.getBytes(0, MODE.length( ), buffer, p); p += MODE.length( ); buffer[p++] = 0; // null terminate /* Send Read Request to tftp server */ outp.setLength(p); sock.send(outp); /* Loop reading data packets from the server until a short * packet arrives; this indicates the end of the file. */ int len = 0; do { sock.receive(inp); if (debug) System.err.println( "Packet # " + Byte.toString(buffer[OFFSET_PACKETNUM])+ "RESPONSE CODE " + Byte.toString(buffer[OFFSET_REQUEST])); if (buffer[OFFSET_REQUEST] == OP_ERROR) { System.err.println("remcat ERROR: " + new String(buffer, 4, inp.getLength( )-4)); return; } if (debug) System.err.println("Got packet of size " + inp.getLength( )); /* Print the data from the packet */ System.out.write(buffer, 4, inp.getLength( )-4); /* Ack the packet. The block number we * want to ack is already in buffer so * we just change the opcode. The ACK is * sent to the port number which the server * just sent the data from, NOT to port * TFTP_PORT. */ buffer[OFFSET_REQUEST] = OP_ACK; outp.setLength(4); outp.setPort(inp.getPort( )); sock.send(outp); } while (inp.getLength( ) == PACKET); if (debug) System.err.println("** ALL DONE** Leaving loop, last size " + inp.getLength( )); } }
To test this client,
you would need a TFTP server.
If you are on a Unix system that you administer, you can enable the
TFTP server to test this client just by editing the file
/etc/inetd.conf
and restarting (or just
reloading, with kill -HUP) the
inetd server. inetd is a
program that listens for a wide range of connections and starts the
servers only when a connection from a client comes along (a kind of
lazy evaluation). Beware of security holes; don’t turn a TFTP
server loose on the Internet without first reading a good security
book, such as O’Reilly’s Building Internet
Firewalls. I set up the traditional
/tftpboot
directory, put this line in my
inetd.conf
, and reloaded
inetd:
tftp dgram udp wait root /usr/libexec/tftpd tftpd -s /tftpboot
(On MS Windows/NT, you could probably enable this from the Services
Control Panel.) Then I put a few test files, one of them named
foo
, into the /tftpboot
directory. Running:
$ java RemCat localhost foo
produced what looked like the file. But just to be safe, I tested the
output of RemCat
against the original file, using
the Unix diff comparison program. No news is
good news:
$ java RemCat localhost foo | diff - /tftpboot/foo
So far so good. Let’s not slip this program on an unsuspecting network without exercising the error handling at least briefly:
$ java RemCat localhost nosuchfile remcat ERROR: File not found $
[34] When the application doesn’t care, these are usually made up by the operating system. For example, when you call a company from a pay phone or cell phone, the company doesn’t usually care what number you are calling from, and if it does, there are ways to find out. Generated port numbers generally range from 1024 (the first non-privileged port; see Chapter 16) to 65535 (the largest value that can be held in a 16-bit port number).
3.141.27.74