/* * Copyright 1999-2006 University of Chicago * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.dcache.ftp.client.vanilla; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.UnknownHostException; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.LinkedList; import org.dcache.ftp.client.DataSink; import org.dcache.ftp.client.DataSource; import org.dcache.ftp.client.HostPort; import org.dcache.ftp.client.HostPort6; import org.dcache.ftp.client.Options; import org.dcache.ftp.client.Session; import org.dcache.ftp.client.dc.ActiveConnectTask; import org.dcache.ftp.client.dc.DataChannelFactory; import org.dcache.ftp.client.dc.LocalReply; import org.dcache.ftp.client.dc.PassiveConnectTask; import org.dcache.ftp.client.dc.SimpleDataChannelFactory; import org.dcache.ftp.client.dc.SimpleTransferContext; import org.dcache.ftp.client.dc.Task; import org.dcache.ftp.client.dc.TaskThread; import org.dcache.ftp.client.dc.TransferContext; import org.dcache.ftp.client.exception.ClientException; import org.dcache.ftp.client.exception.FTPException; import org.dcache.ftp.client.exception.FTPReplyParseException; import org.dcache.ftp.client.exception.ServerException; import org.dcache.util.NetworkUtils; import org.dcache.util.PortRange; /** * <b> * This class is not ment directly for the users. * </b> * This class represents the part of the client responsible for data * channel management. Especially when the remote server is in the passive * mode, it behaves a lot like a local server. Thus its interface looks * very much like a server interface. * <br> * Current implementation is multithreaded. One thread is used for thread * management and one for each transfer (this makes sense in GridFTP * parallelism). * <br> * The public methods can generally be divided into setter methods and active * methods. Active methods are setActive(), setPassive(), retrieve(), * and store(), and setter methods are the remaining. * Setter methods do not generally throw exceptions related to ftp. * Settings are not checked for correctness until the server * is asked to performed some action, which is done by active methods. * So you are safe to cal setXX() methods with any argument you like, until * you call one of the "active" methods mentioned above. * <br> * The managing thread is not started until one of the "active" methods is * called: setActive(), retrieve(), or store(). These methods * are asynchronous (return before completion) and the action is undertaken * by the local manager thread. From this point on, all communication * back to the caller is done through unidirectional local control * channel. Information is communicated back to the user in form of FTP * replies (instances of LocalReply). Generally, the sequence of * replies should be the same as when communicating with remote server * during the transfer (1xx intermediary reply; markers; final 226). * Exceptions are serialized into 451 negative reply. **/ public class FTPServerFacade { private static final Logger logger = LoggerFactory.getLogger(FTPServerFacade.class); /** * local server socket parameter; used in setPassive() **/ public static final int DEFAULT_QUEUE = 100; protected Session session; protected final LocalControlChannel localControlChannel; protected DataChannelFactory dataChannelFactory; protected ServerSocket serverSocket; protected final FTPControlChannel remoteControlChannel; protected HostPort remoteServerAddress; // used only by FTPServerFacade private TaskThread taskThread; /** * Data channels are operated in multithreaded manner and they pass * information (including exceptions) to the user using the local * control channel. In the unlikely event that it fails, there is no * way to communicate the exception to the user. In such circumstances * this method should be called to print the exception directly to console. **/ public static void cannotPropagateError(Throwable e) { logger.error("Exception occured in the exception handling " + "code, so it cannot be properly propagated to " + "the user", e); } public FTPServerFacade(FTPControlChannel remoteControlChannel) { this.remoteControlChannel = remoteControlChannel; this.session = new Session(); this.localControlChannel = new LocalControlChannel(); this.dataChannelFactory = new SimpleDataChannelFactory(); } /** * Use this method to get the client end of the local * control channel. It is the only way to get the * information of the current transfer state. **/ public BasicClientControlChannel getControlChannel() { return localControlChannel; } /** * @return the session object associated with this server **/ public Session getSession() { return session; } // unconditional authorization /** * No need for parameters; locally you are always authorized. **/ public void authorize() { session.authorized = true; } public void setTransferType(int type) { session.transferType = type; } public void setTransferMode(int mode) { session.transferMode = mode; } public void setProtectionBufferSize(int size) { session.protectionBufferSize = size; } /** * Do nothing; this class does not support any options **/ public void setOptions(Options opts) { } /** * Behave like setPassive(ANY_PORT, DEFAULT_QUEUE) **/ public HostPort setPassive() throws IOException { return setPassive(new PortRange(0), DEFAULT_QUEUE); } /** * Start the local server * * @param range required server port range * @param queue max size of queue of awaiting new connection * requests * @return the server address **/ public HostPort setPassive(PortRange range, int queue) throws IOException { if (serverSocket == null) { ServerSocketChannel channel = ServerSocketChannel.open(); range.bind(channel.socket(), queue); serverSocket = channel.socket(); } session.serverMode = Session.SERVER_PASSIVE; String address = NetworkUtils.getLocalAddress(InetAddress.getByName(remoteControlChannel.getHost())).getHostAddress(); int localPort = serverSocket.getLocalPort(); if (remoteControlChannel.isIPv6()) { String version = HostPort6.getIPAddressVersion(address); session.serverAddress = new HostPort6(version, address, localPort); } else { session.serverAddress = new HostPort(address, localPort); } logger.debug("started passive server at port " + session.serverAddress.getPort()); return session.serverAddress; } /** * Asynchronous; return before completion. * Connect to the remote server. * Any exception that would occure will not be thrown but * returned through the local control channel. **/ public void setActive(HostPort hp) throws UnknownHostException, ClientException, IOException { if (logger.isDebugEnabled()) { logger.debug("hostport: " + hp.getHost() + " " + hp.getPort()); } session.serverMode = Session.SERVER_ACTIVE; this.remoteServerAddress = hp; } /** * Convert the exception to a negative 451 reply, and pipe * it to the control channel. **/ protected void exceptionToControlChannel(Throwable e, String msg) { // this could be reimplemented. // Now the exception is serialized to the control channel. // but it could be simply appended to the LocalReply, // if LocalReply had such functionality. exceptionToControlChannel(e, msg, localControlChannel); } /** * Convert the exception to a negative 451 reply, and pipe * it to the provided control channel. **/ public static void exceptionToControlChannel( Throwable e, String msg, BasicServerControlChannel control) { // how to convert exception stack trace to string? // i am sure it can be done easier. java.io.StringWriter writer = new java.io.StringWriter(); e.printStackTrace(new java.io.PrintWriter(writer)); String stack = writer.toString(); // 451 Requested action aborted: local error in processing. LocalReply reply = new LocalReply(451, msg + "\n" + e.toString() + "\n" + stack); control.write(reply); } /** * Asynchronous; return before completion. * Start the incoming transfer and * store the file to the supplied data sink. * Any exception that would occure will not be thrown but * returned through the local control channel. **/ public void store(DataSink sink) { try { localControlChannel.resetReplyCount(); TransferContext context = createTransferContext(); if (session.serverMode == Session.SERVER_PASSIVE) { runTask(createPassiveConnectTask(sink, context)); } else { runTask(createActiveConnectTask(sink, context)); } } catch (Exception e) { exceptionToControlChannel(e, "ocurred during store()"); } } /** * Asynchronous; return before completion. * Start the outgoing transfer * reading the data from the supplied data source. * Any exception that would occure will not be thrown but * returned through the local control channel. **/ public void retrieve(DataSource source) { try { localControlChannel.resetReplyCount(); TransferContext context = createTransferContext(); if (session.serverMode == Session.SERVER_PASSIVE) { runTask(createPassiveConnectTask(source, context)); } else { runTask(createActiveConnectTask(source, context)); } } catch (Exception e) { exceptionToControlChannel(e, "ocurred during retrieve()"); } } /** * close data channels, but not control, nor the server **/ public void abort() throws IOException { } protected void transferAbort() { if (session.serverMode == Session.SERVER_PASSIVE) { unblockServer(); stopTaskThread(); } } protected void unblockServer() { if (serverSocket == null) { return; } try { // this is a hack to ensue the server socket is // unblocked from accpet() // but this is not guaranteed to work still try (SocketChannel channel = SocketChannel.open(serverSocket.getLocalSocketAddress())) { channel.socket().getInputStream(); } } catch (Exception e) { } } public void close() throws IOException { logger.debug("close data channels"); abort(); logger.debug("close server socket"); if (serverSocket != null) { try { serverSocket.close(); } catch (IOException e) { } unblockServer(); } stopTaskThread(); } /** * Use this as an interface to the local manager thread. * This submits the task to the thread queue. * The thread will perform it when it's ready with other * waiting tasks. **/ private synchronized void runTask(Task task) { if (taskThread == null) { taskThread = new TaskThread(); } taskThread.runTask(task); } protected synchronized void stopTaskThread() { logger.debug("stop master thread"); if (taskThread != null) { taskThread.stop(); taskThread.join(); taskThread = null; } } // task "factories": // use these methods to create tasks private PassiveConnectTask createPassiveConnectTask(DataSource source, TransferContext context) { return new PassiveConnectTask(serverSocket, source, localControlChannel, session, dataChannelFactory, context); } private PassiveConnectTask createPassiveConnectTask(DataSink sink, TransferContext context) { return new PassiveConnectTask(serverSocket, sink, localControlChannel, session, dataChannelFactory, context); } private ActiveConnectTask createActiveConnectTask(DataSource source, TransferContext context) { return new ActiveConnectTask(this.remoteServerAddress, source, localControlChannel, session, dataChannelFactory, context); } private ActiveConnectTask createActiveConnectTask(DataSink sink, TransferContext context) { return new ActiveConnectTask(this.remoteServerAddress, sink, localControlChannel, session, dataChannelFactory, context); } // inner classes /** * This inner class represents a local control channel. * One process can write replies using BasicServerControlChannel * interface, and the other can read replies using * BasicClientControlChannel interface. **/ protected class LocalControlChannel extends BasicClientControlChannel implements BasicServerControlChannel { // FIFO queue of Replies private LinkedList replies = null; // how many replies have been pushed so far private int replyCount = 0; public LocalControlChannel() { replies = new LinkedList(); } protected synchronized void push(Reply newReply) { replies.add(newReply); replyCount++; notify(); } // blocking pop from queue protected synchronized Reply pop() throws InterruptedException { while (replies.isEmpty()) { wait(); } return (Reply) replies.removeFirst(); } //non blocking; check if queue is ready for pop public synchronized boolean ready() { return (!replies.isEmpty()); } @Override public synchronized int getReplyCount() { return replyCount; } @Override public synchronized void resetReplyCount() { replies.clear(); replyCount = 0; } @Override public Reply read() throws IOException, FTPReplyParseException, ServerException { try { return pop(); } catch (InterruptedException e) { ServerException se = new ServerException(FTPException.UNSPECIFIED, "interrupted while waiting."); se.setRootCause(e); throw se; } } @Override public void write(Reply reply) { push(reply); } @Override public void waitFor(Flag aborted, int ioDelay, int maxWait) throws ServerException, IOException, InterruptedException { int i = 0; logger.debug("waiting for reply in local control channel"); while (!ready()) { if (aborted.flag) { throw new InterruptedException(); } logger.debug("slept " + i); Thread.sleep(ioDelay); i += ioDelay; if (maxWait != WAIT_FOREVER && i >= maxWait) { logger.debug("timeout"); throw new ServerException(ServerException.REPLY_TIMEOUT); } } logger.debug("local control channel ready"); } @Override public void abortTransfer() { transferAbort(); } }// class localControlChannel protected TransferContext createTransferContext() { return SimpleTransferContext.getDefault(); } } // FTPServerFacade