/* * 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.BufferedReader; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketTimeoutException; import java.nio.charset.StandardCharsets; import org.dcache.ftp.client.exception.FTPReplyParseException; import org.dcache.ftp.client.exception.ServerException; import org.dcache.ftp.client.exception.UnexpectedReplyCodeException; /** * <p> * Represents FTP Protocol Interpreter. Encapsulates * control channel communication. * <p> * </p> */ public class FTPControlChannel extends BasicClientControlChannel { private static final Logger logger = LoggerFactory.getLogger(FTPControlChannel.class); public static final String CRLF = "\r\n"; // used in blocking waitForReply() private static final int WAIT_FOREVER = -1; protected Socket socket; //input stream protected BufferedReader ftpIn; //raw stream underlying ftpIn protected InputStream rawFtpIn; //output stream protected OutputStream ftpOut; protected String host; protected final int port; //true if connection has already been opened. protected boolean hasBeenOpened = false; private final boolean ipv6; private Reply lastReply; public FTPControlChannel(String host, int port) { this.host = host; this.port = port; this.ipv6 = (this.host.indexOf(':') != -1); } public String getHost() { return this.host; } public int getPort() { return this.port; } public boolean isIPv6() { return this.ipv6; } // not intended to be public. you can set streams in the constructor. protected void setInputStream(InputStream in) { rawFtpIn = in; ftpIn = new BufferedReader(new InputStreamReader(rawFtpIn)); } protected void setOutputStream(OutputStream out) { ftpOut = out; } /** * opens the connection and returns after it is ready for communication. * Before returning, it intercepts the initial server reply(-ies), * and not positive, throws UnexpectedReplyCodeException. * After returning, there should be no more queued replies on the line. * <p> * Here's the sequence for connection establishment (rfc959): * <PRE> * 120 * 220 * 220 * 421 * </PRE> * * @throws IOException on I/O error * @throws ServerException on negative or faulty server reply **/ public void open() throws IOException, ServerException { if (hasBeenOpened()) { throw new IOException("Attempt to open an already opened connection"); } InetAddress allIPs[]; //depending on constructor used, we may already have streams if (!haveStreams()) { boolean found = false; int i = 0; boolean firstPass = true; allIPs = InetAddress.getAllByName(host); while (!found) { try { logger.debug("opening control channel to " + allIPs[i] + " : " + port); InetSocketAddress isa = new InetSocketAddress(allIPs[i], port); socket = new Socket(); socket.connect(isa); found = true; } catch (IOException ioEx) { logger.debug("failed connecting to " + allIPs[i] + " : " + port + ":" + ioEx); i++; if (i == allIPs.length) { if (firstPass) { firstPass = false; i = 0; } else { throw ioEx; } } } } host = socket.getInetAddress().getHostAddress(); setInputStream(socket.getInputStream()); setOutputStream(socket.getOutputStream()); } readInitialReplies(); hasBeenOpened = true; } //intercepts the initial replies //(that the server sends after opening control ch.) protected void readInitialReplies() throws IOException, ServerException { Reply reply = null; try { reply = read(); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException( rpe, "Received faulty initial reply"); } if (Reply.isPositivePreliminary(reply)) { try { reply = read(); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException( rpe, "Received faulty second reply"); } } if (!Reply.isPositiveCompletion(reply)) { close(); throw ServerException.embedUnexpectedReplyCodeException( new UnexpectedReplyCodeException(reply), "Server refused connection."); } } /** * Returns the last reply received from the server. */ public Reply getLastReply() { return lastReply; } /** * Closes the control channel */ public void close() throws IOException { logger.debug("ftp socket closed"); if (ftpIn != null) ftpIn.close(); if (ftpOut != null) ftpOut.close(); if (socket != null) socket.close(); hasBeenOpened = false; } private int checkSocketDone(Flag aborted, int ioDelay, int maxWait) throws ServerException, IOException, InterruptedException { int oldTOValue = this.socket.getSoTimeout(); int c = -10; int time = 0; boolean done = false; if (ioDelay <= 0) { ioDelay = 2000; } while (!done) { try { if (aborted.flag) { throw new InterruptedException(); } this.socket.setSoTimeout(ioDelay); ftpIn.mark(2); c = ftpIn.read(); done = true; } catch (SocketTimeoutException e) { // timeouts will happen logger.debug("temp timeout" + e); } catch (Exception e) { throw new InterruptedException(); } finally { ftpIn.reset(); this.socket.setSoTimeout(oldTOValue); } time += ioDelay; if (time > maxWait && maxWait != WAIT_FOREVER) { throw new ServerException(ServerException.REPLY_TIMEOUT); } } return c; } /** * Block until one of the conditions are true: * <ol> * <li> a reply is available in the control channel, * <li> timeout (maxWait) expired * <li> aborted flag changes to true. * </ol> * If maxWait == WAIT_FOREVER, never timeout * and only check conditions (1) and (3). * * @param maxWait timeout in miliseconds * @param ioDelay frequency of polling the control channel * and checking the conditions * @param aborted flag indicating wait aborted. **/ @Override public void waitFor(Flag aborted, int ioDelay, int maxWait) throws ServerException, IOException, InterruptedException { int oldTimeout = this.socket.getSoTimeout(); try { int c = 0; if (maxWait != WAIT_FOREVER) { this.socket.setSoTimeout(maxWait); } else { this.socket.setSoTimeout(0); } c = this.checkSocketDone(aborted, ioDelay, maxWait); /* A bug in the server causes it to append \0 to each reply. As the result, we receive this \0 before the next reply. The code below handles this case. */ if (c != 0) { // if we're here, the server is healthy // and the reply is waiting in the buffer return; } // if we're here, we deal with the buggy server. // we discarded the \0 and now resume wait. logger.debug("Server sent \\0; resume wait"); try { // gotta read past the 0 we just remarked c = ftpIn.read(); c = this.checkSocketDone(aborted, ioDelay, maxWait); } catch (SocketTimeoutException e) { throw new ServerException(ServerException.REPLY_TIMEOUT); } catch (EOFException e) { throw new InterruptedException(); } } finally { this.socket.setSoTimeout(oldTimeout); } } /** * Block until a reply is available in the control channel. * * @return the first unread reply from the control channel. * @throws IOException on I/O error * @throws FTPReplyParseException on malformatted server reply **/ @Override public Reply read() throws ServerException, IOException, FTPReplyParseException, EOFException { Reply reply = new Reply(ftpIn); //System.out.println("FTP IN string "+reply.toString()); if (logger.isDebugEnabled()) { logger.debug("Control channel received: " + reply); } lastReply = reply; return reply; } @Override public void abortTransfer() { } /** * Sends the command over the control channel. * Do not wait for reply. * * @param cmd FTP command * @throws java.io.IOException on I/O error */ public void write(Command cmd) throws IOException, IllegalArgumentException { //we delete the initial reply when the first command is sent if (cmd == null) { throw new IllegalArgumentException("null argument: cmd"); } if (logger.isDebugEnabled()) { logger.debug("Control channel sending: " + cmd); } ftpOut.write(cmd.toString().getBytes(StandardCharsets.US_ASCII)); ftpOut.flush(); } /** * Write the command to the control channel, * block until reply arrives and return the reply. * Before calling this method make sure that no old replies are * waiting on the control channel. Otherwise the reply returned * may not be the reply to this command. * * @param cmd FTP command * @return the first reply that waits in the control channel * @throws java.io.IOException on I/O error * @throws FTPReplyParseException on bad reply format **/ public Reply exchange(Command cmd) throws ServerException, IOException, FTPReplyParseException { // send the command write(cmd); // get the reply return read(); } /** * Write the command to the control channel, * block until reply arrives and check if the command * completed successfully (reply code 200). * If so, return the reply, otherwise throw exception. * Before calling this method make sure that no old replies are * waiting on the control channel. Otherwise the reply returned * may not be the reply to this command. * * @param cmd FTP command * @return the first reply that waits in the control channel * @throws java.io.IOException on I/O error * @throws FTPReplyParseException on bad reply format * @throws UnexpectedReplyCodeException if reply is not a positive * completion reply (code 200) **/ public Reply execute(Command cmd) throws ServerException, IOException, FTPReplyParseException, UnexpectedReplyCodeException { Reply reply = exchange(cmd); // check for positive reply if (!Reply.isPositiveCompletion(reply)) { throw new UnexpectedReplyCodeException(reply); } return reply; } public InetSocketAddress getLocalAddress() { return (InetSocketAddress) socket.getLocalSocketAddress(); } public InetSocketAddress getRemoteAddress() { return (InetSocketAddress) socket.getRemoteSocketAddress(); } protected boolean hasBeenOpened() { return hasBeenOpened; } protected boolean haveStreams() { return (ftpIn != null && ftpOut != null); } } // end StandardPI