package org.dcache.ftp.data; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; import java.util.regex.Pattern; import org.dcache.pool.repository.RepositoryChannel; /** * Implementation of MODE X. * */ public class ModeX extends Mode { enum SenderState { /** Wait for receiver to send READY. */ WAIT_READY, /** Wait for receiver to send BYE. */ WAIT_BYE, /** Prepare next block for transmission. */ NEXT_BLOCK, /** Send the block header. */ SEND_HEADER, /** Send the block data. */ SEND_DATA } enum ReceiverState { /** Send BYE message. */ SEND_BYE, /** Read block header. */ READ_HEADER, /** Read block data. */ READ_DATA } /** * Header length of a mode E block. */ public static final int HEADER_LENGTH = 25; public static final int EOF_DESCRIPTOR = 64; public static final int EOD_DESCRIPTOR = 8; public static final int SENDER_CLOSES_THIS_STREAM_DESCRIPTOR = 4; public static final int KNOWN_DESCRIPTORS = EOF_DESCRIPTOR | EOD_DESCRIPTOR | SENDER_CLOSES_THIS_STREAM_DESCRIPTOR; /** * The chunk size used when sending files. * * Large blocks will reduce the overhead of sending. However, in * case of multible concurrent streams, large blocks will make * disk access less sequential on both the sending and receiving * side. */ private final int _blockSize; /** * Position in file when sending data. Used by Sender. */ private long _currentPosition; /** * Number of not yet transferred. Used by Sender. */ private long _currentCount; /** * True iff EOF was sent or received. */ private boolean _eof; /** * Count how many EODs have been sent. Used by the sender to * ensure that we maintain at least one data channel until the * transfer has completed. */ private int _closing; /** * */ private static final Charset _ascii = Charset.forName("ascii"); /** * */ private static final CharsetEncoder _encoder = _ascii.newEncoder(); /** * */ private static final CharsetDecoder _decoder = _ascii.newDecoder(); /** * Implementation of send in mode X. There will be an instance per * data channel. The sender repeatedly bites _blockSize bytes of * the file and transfers it as a single block. I.e. * _currentPosition is incremented by _blockSize bytes at a time. */ private class Sender extends AbstractMultiplexerListener { /** The data channel. */ protected final SocketChannel _socket; /** Current state of the sender. */ protected SenderState _state; /** Current file position from which we send. */ protected long _position; /** Number of bytes left to send from the current block. */ protected long _count; /** True if receiver has requested the channel to be closed. */ protected boolean _closeAtNextBlock; /** Buffer for representing a block header. */ protected final ByteBuffer _header = ByteBuffer.allocate(HEADER_LENGTH); /** Buffer for reading commands from the receiver. */ protected final ByteBuffer _command = ByteBuffer.allocate(128); /** Buffer for reading commands from the receiver. */ protected final CharBuffer _decodedCommand = CharBuffer.allocate(128); public Sender(SocketChannel socket) { _socket = socket; _state = SenderState.WAIT_READY; _closeAtNextBlock = false; } @Override public void register(Multiplexer multiplexer) throws IOException { multiplexer.register(this, SelectionKey.OP_READ, _socket); } @Override public void read(Multiplexer multiplexer, SelectionKey key) throws IOException, FTPException, InterruptedException { /* Protect against clients sending large commands. We * could enlarge the command buffer, but this would open * the server to DOS attacks from clients sending very * large commands. */ if (!_command.hasRemaining()) { throw new FTPException("Command buffer full"); } /* Read available data. */ long nbytes = _socket.read(_command); if (nbytes == -1) { if (_state == SenderState.WAIT_READY) { /* From the GridFTP v2 spec: "Passive receiver may * close new data socket without sending 'READY' * message or even stop accepting new * connections." * * We extend this to also cover active receivers. */ close(multiplexer, key, _eof); return; } else { throw new FTPException("Lost connection"); } } /* Decode buffer. */ _command.flip(); _decoder.decode(_command, _decodedCommand, false); _command.compact(); /* Remove first line from buffer. */ char c; StringBuffer line = new StringBuffer(); _decodedCommand.flip(); do { /* Return early if command is incomplete. */ if (!_decodedCommand.hasRemaining()) { _decodedCommand.limit(_decodedCommand.capacity()); return; } c = _decodedCommand.get(); line.append(c); } while (c != '\n'); _decodedCommand.compact(); /* Split line into arguments. */ String[] arg = Pattern.compile("\\s").split(line); if (arg.length == 0) { throw new FTPException("Empty command received (protocol violation)"); } /* Interpret command. */ String cmd = arg[0]; if (cmd.equals("READY") && _state == SenderState.WAIT_READY) { _state = SenderState.NEXT_BLOCK; key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); } else if (cmd.equals("BYE") && _state == SenderState.WAIT_BYE) { // shut down close(multiplexer, key, _eof); } else if (cmd.equals("CLOSE")) { // shutdown the channel at the end of the current block _closeAtNextBlock = true; } else if (cmd.equals("RESEND")) { // resend a block throw new FTPException("RESEND is not implemented"); } else { throw new FTPException("Unexpected command '" + cmd + "' in state " + _state); } } @Override public void write(Multiplexer multiplexer, SelectionKey key) throws IOException, FTPException { switch (_state) { case NEXT_BLOCK: _position = _currentPosition; _count = Math.min(_currentCount, _blockSize); /* Prepare header. */ byte descriptor; if (_count == 0) { // No more data. Send EOD and EOF. descriptor = (byte)(EOF_DESCRIPTOR | EOD_DESCRIPTOR | SENDER_CLOSES_THIS_STREAM_DESCRIPTOR); _closing++; _eof = true; } else if (_closeAtNextBlock && _opened > _closing + 1) { // Receiver requested close. Notice that we only // honor the close request as long as at least one // data channel remains open. descriptor = (byte)(EOD_DESCRIPTOR | SENDER_CLOSES_THIS_STREAM_DESCRIPTOR); _closing++; _count = 0; } else { // Regular block. descriptor = 0; _currentPosition += _count; _currentCount -= _count; } _header.clear(); _header.put(descriptor); _header.putLong(_count); _header.putLong(_position); _header.putLong(0); // Transaction ID _header.flip(); _state = SenderState.SEND_HEADER; case SEND_HEADER: _socket.write(_header); if (_header.position() < _header.limit()) { break; } /* If at end of stream, stop subscription for write * events and wait for BYE from receiver. */ if (_count == 0) { key.interestOps(SelectionKey.OP_READ); _state = SenderState.WAIT_BYE; break; } _state = SenderState.SEND_DATA; case SEND_DATA: long nbytes = transferTo(_position, _count, _socket); _monitor.sentBlock(_position, nbytes); _position += nbytes; _count -= nbytes; if (_count == 0) { _state = SenderState.NEXT_BLOCK; } break; } } } class Receiver extends AbstractMultiplexerListener { /** The data channel. */ protected final SocketChannel _socket; /** Current state of the receiver. */ protected ReceiverState _state; /** Current position in file. */ protected long _position; /** Number of bytes left to receive from the current block. */ protected long _count; /** The flags of the last block header. */ protected int _flags; /** Buffer for representing a block header. */ protected final ByteBuffer _header = ByteBuffer.allocate(HEADER_LENGTH); /** Buffer for reading commands from the receiver. */ protected final ByteBuffer _command = ByteBuffer.allocate(128); public Receiver(SocketChannel socket) { _socket = socket; _count = 0; _position = 0; _flags = 0; _state = ReceiverState.READ_HEADER; _command.limit(0); } private void addCommand(String s) { CharBuffer buffer = CharBuffer.allocate(s.length() + 1); buffer.put(s); buffer.put('\n'); buffer.flip(); _command.compact(); _encoder.encode(buffer, _command, true); _command.flip(); } @Override public void register(Multiplexer multiplexer) throws IOException { multiplexer.register(this, SelectionKey.OP_WRITE, _socket); addCommand("READY"); } @Override public void write(Multiplexer multiplexer, SelectionKey key) throws IOException { try { _socket.write(_command); if (_command.position() == _command.limit()) { if (_state == ReceiverState.SEND_BYE) { close(multiplexer, key, true); // TODO: Check true } else { key.interestOps(SelectionKey.OP_READ); } } } catch (ClosedChannelException e) { /* From GridFTP v2 spec: "...the sender may choose not * to wait for 'BYE' acknowledgement. The sender is * allowed to close data channels immediately after * sending EOD, and the receiver may get a socket * error trying to send 'BYE' message back to the * sender". */ if (_state == ReceiverState.SEND_BYE) { close(multiplexer, key, true); // TODO: Check true } else { throw e; } } } @Override public void read(Multiplexer multiplexer, SelectionKey key) throws IOException, FTPException, InterruptedException { long nbytes; switch (_state) { case READ_HEADER: /* Read header. */ nbytes = _socket.read(_header); if (nbytes == -1) { /* Stream was closed. A sender must always send * EOD on a channel before closing it. We * therefore consider the end of stream to be an * error. */ throw new FTPException("Stream ended before EOD"); } if (_header.position() < _header.limit()) { /* Incomplete header. */ return; } _header.rewind(); _flags = _header.get(); _count = _header.getLong(); _position = _header.getLong(); _header.clear(); /* The GridFTP spec states that we should generate an * error whenever we receive a descriptor we don't * know how to handle. */ if ((_flags & ~KNOWN_DESCRIPTORS) != 0) { throw new FTPException("Received block with unknown descriptor (" + _flags + ")"); } /* At least one EOF must be received. The transfer has * been complete when EOF was received and all open * channels have been closed. */ if ((_flags & EOF_DESCRIPTOR) != 0) { _eof = true; } /* Empty blocks are allowed. */ if (_count == 0) { /* If EOD was received then send BYE message. */ if ((_flags & EOD_DESCRIPTOR) != 0) { key.interestOps(SelectionKey.OP_WRITE); addCommand("BYE"); _state = ReceiverState.SEND_BYE; } break; } _monitor.preallocate(_position + _count); _state = ReceiverState.READ_DATA; case READ_DATA: /* Receive data. */ nbytes = transferFrom(_socket, _position, _count); if (nbytes == -1) { throw new FTPException("Stream was closed in the middle of a block"); } _monitor.receivedBlock(_position, nbytes); _position += nbytes; _count -= nbytes; if (_count == 0) { /* If EOD was received then send BYE message. */ if ((_flags & EOD_DESCRIPTOR) != 0) { key.interestOps(SelectionKey.OP_WRITE); addCommand("BYE"); _state = ReceiverState.SEND_BYE; } else { _state = ReceiverState.READ_HEADER; } } break; } } } public ModeX(Role role, RepositoryChannel file, ConnectionMonitor monitor, int blockSize) throws IOException { super(role, file, monitor); _currentPosition = getStartPosition(); _currentCount = getSize(); _eof = false; _closing = 0; _blockSize = blockSize; } @Override public void newConnection(Multiplexer multiplexer, SocketChannel socket) throws IOException { switch (_role) { case Sender: multiplexer.add(new Sender(socket)); break; case Receiver: /* From the GridFTP 2 spec: "After receiving EOF block * from a sender host, active data receiver host must not * try to open any new data channels to that sender host." * * This rule is difficult to honor, as we may have a * connection establishment "in progress" at the time we * received the EOF. At the same time it is not clear if * an active receiver is allowed to close a connection * before READY has been sent; passive receivers are * explicitly allowed to do so. */ if (_eof) { socket.close(); } else { multiplexer.add(new Receiver(socket)); } break; } } }