de.unkrig.commons.net.ftp.FtpSession Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of de-unkrig-commons Show documentation
Show all versions of de-unkrig-commons Show documentation
A versatile Java(TM) library that implements many useful container and utility classes.
/*
* de.unkrig.commons - A general-purpose Java class library
*
* Copyright (c) 2012, Arno Unkrig
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
* following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
* following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package de.unkrig.commons.net.ftp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.unkrig.commons.io.IoUtil;
import de.unkrig.commons.io.LineUtil;
import de.unkrig.commons.lang.protocol.ConsumerWhichThrows;
import de.unkrig.commons.lang.protocol.ProducerWhichThrows;
import de.unkrig.commons.lang.protocol.RunnableWhichThrows;
import de.unkrig.commons.net.ftp.FtpServer.CommandCode;
import de.unkrig.commons.net.ftp.ftplett.Ftplett;
import de.unkrig.commons.nullanalysis.Nullable;
/**
* Representation of one FTP session, i.e. control connection and its state.
*/
public
class FtpSession implements RunnableWhichThrows {
public
FtpSession(
InputStream controlIn,
OutputStream controlOut,
InetSocketAddress controlLocalSocketAddress,
InetAddress controlRemoteAddress,
Ftplett ftplett
) {
this.controlIn = LineUtil.lineProducerISO8859_1(controlIn);
this.controlOut = LineUtil.lineConsumerISO8859_1(controlOut);
this.interfacE = controlLocalSocketAddress.getAddress();
this.activeDataRemoteSocketAddress = new InetSocketAddress(
controlRemoteAddress,
controlLocalSocketAddress.getPort() - 1
);
this.ftplett = ftplett;
}
@Override public void
run() throws IOException {
this.sendReply("220 de.unkrig.commons.net.ftp.FtpServer");
int state = 0;
try {
for (;;) {
final Command c = this.receiveCommand();
if (c == null) return;
final String argument = c.argument;
if (state < 1 && c.code == CommandCode.PASS) {
this.sendReply("503 Login with USER first.");
continue;
}
if (state < 2 && c.code != CommandCode.USER && c.code != CommandCode.PASS) {
this.sendReply("530 Please login with USER and PASS.");
continue;
}
switch (c.code) {
case SYST:
this.sendReply("215 " + System.getProperty("os.name"));
break;
case USER:
if (c.argument == null) {
this.sendReply("501 'user': Invalid number of parameters.");
break;
}
state = 1;
this.sendReply("331 Password required for " + c.argument + ".");
break;
case PASS:
state = 2;
this.sendReply("230 User logged in.");
break;
case CWD:
if (argument == null) {
this.sendReply("501 Invalid number of parameters.");
break;
}
if (this.ftplett.changeWorkingDirectory(argument)) {
this.sendReply("250 cwd command successful.");
} else {
this.sendReply("550 Requested action not taken");
}
break;
case PWD:
this.sendReply("257 \"" + this.ftplett.getWorkingDirectory() + "\" is current directory.");
break;
case NOOP:
this.sendReply("200 noop command successful.");
break;
case MODE:
if (c.argument == null) {
this.sendReply("501 Invalid number of parameters.");
break;
}
if ("S".equals(c.argument)) {
this.transmissionMode = TransmissionMode.STREAM;
this.sendReply("200 Stream transmission mode selected");
} else
if ("B".equals(c.argument)) {
this.transmissionMode = TransmissionMode.BLOCK;
this.sendReply("200 Block transmission mode selected");
} else
if ("C".equals(c.argument)) {
this.transmissionMode = TransmissionMode.COMPRESSED;
this.sendReply("200 Compressed transmission mode selected");
} else
{
this.sendReply("501 Invalid transmission mode '" + c.argument + "'");
}
break;
case RETR:
if (argument == null) {
this.sendReply("501 Invalid number of parameters.");
break;
}
final InputStream is = this.ftplett.retrieve(argument);
if (is == null) {
this.sendReply("550 The system cannot find the file specified.");
break;
}
try {
this.dataConnection(new ConsumerWhichThrows() {
@Override public void
consume(Socket dataSocket) throws IOException {
long count = IoUtil.copy(is, dataSocket.getOutputStream());
LOGGER.fine(count + " bytes sent");
}
});
} finally {
try { is.close(); } catch (Exception e) {}
}
break;
case LIST: // http://tools.ietf.org/html/rfc959#page-32
this.dataConnection(new ConsumerWhichThrows() {
@Override public void
consume(Socket dataSocket) throws IOException {
final Writer w = new OutputStreamWriter(
dataSocket.getOutputStream(),
Charset.forName("ISO-8859-1")
);
ConsumerWhichThrows lineConsumer = (
new ConsumerWhichThrows() {
@Override public void
consume(String line) throws IOException {
w.write(line + "\r\n");
}
}
);
if (!FtpSession.this.ftplett.list(argument, lineConsumer)) {
FtpSession.this.sendReply("550 '" + c.argument + "' does not exist");
}
w.flush();
}
});
break;
case NLST: // http://tools.ietf.org/html/rfc959#page-33
if (argument == null) {
this.sendReply("501 Invalid number of parameters.");
break;
}
this.dataConnection(new ConsumerWhichThrows() {
@Override public void
consume(Socket dataSocket) throws IOException {
final Writer w = new OutputStreamWriter(
dataSocket.getOutputStream(),
Charset.forName("ISO-8859-1")
);
ConsumerWhichThrows lineConsumer = (
new ConsumerWhichThrows() {
@Override public void
consume(String line) throws IOException {
w.write(line + "\r\n");
}
}
);
if (!FtpSession.this.ftplett.nameList(argument, lineConsumer)) {
FtpSession.this.sendReply("550 '" + c.argument + "' does not exist");
}
w.flush();
}
});
break;
case PORT:
if (c.argument == null) {
this.sendReply("501 'port': Invalid number of parameters.");
break;
}
Matcher matcher = Pattern.compile("(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+)").matcher(c.argument);
if (!matcher.matches()) {
this.sendReply("501 Server cannot accept argument.");
break;
}
this.activeDataRemoteSocketAddress = new InetSocketAddress(
InetAddress.getByAddress(new byte[] {
(byte) Integer.parseInt(matcher.group(1)),
(byte) Integer.parseInt(matcher.group(2)),
(byte) Integer.parseInt(matcher.group(3)),
(byte) Integer.parseInt(matcher.group(4)),
}),
256 * Integer.parseInt(matcher.group(5)) + Integer.parseInt(matcher.group(6))
);
this.passiveDataTransfer = false;
LOGGER.fine("Client address for data connection: " + this.activeDataRemoteSocketAddress);
this.sendReply("200 port command successful.");
break;
case QUIT:
return;
case TYPE:
this.sendReply("200 type command successful.");
break;
case PASV:
{
ServerSocket pdcss = this.passiveDataConnectionServerSocket;
if (pdcss == null) {
pdcss = (this.passiveDataConnectionServerSocket = new ServerSocket(0, 1));
}
InetSocketAddress localSocketAddress = (InetSocketAddress) pdcss.getLocalSocketAddress();
byte[] address = this.interfacE.getAddress();
int port = localSocketAddress.getPort();
this.sendReply(
"227 Entering Passive Mode ("
+ (0xff & address[0]) + ","
+ (0xff & address[1]) + ","
+ (0xff & address[2]) + ","
+ (0xff & address[3]) + ","
+ (0xff & port >> 8) + "," + (0xff & port)
+ ")."
);
this.passiveDataTransfer = true;
}
break;
case STOR:
if (argument == null) {
this.sendReply("501 Invalid number of parameters.");
break;
}
final OutputStream os = this.ftplett.store(argument);
if (os == null) {
this.sendReply("550 The system cannot create the file specified.");
break;
}
try {
this.dataConnection(new ConsumerWhichThrows() {
@Override public void
consume(Socket dataSocket) throws IOException {
long count = IoUtil.copy(dataSocket.getInputStream(), os);
LOGGER.fine(count + " bytes received");
}
});
} finally {
try { os.close(); } catch (Exception e) {}
}
break;
case DELE:
if (argument == null) {
this.sendReply("501 Invalid number of parameters.");
break;
}
if (!this.ftplett.delete(argument)) {
this.sendReply("550 The system cannot delete the file specified.");
break;
}
this.sendReply("250 File deleted");
break;
case RNFR:
if (c.argument == null) {
this.sendReply("501 Invalid number of parameters.");
break;
}
this.renameFrom = c.argument;
this.sendReply("350 Requested file action pending further information.");
break;
case RNTO:
{
if (argument == null) {
this.sendReply("501 Invalid number of parameters.");
break;
}
String rf = this.renameFrom;
assert rf != null;
if (!this.ftplett.rename(rf, argument)) {
this.sendReply("553 The system cannot rename the file specified.");
break;
}
this.sendReply("250 File renamed");
}
break;
case MDTM:
if (argument == null) {
this.sendReply("501 Invalid number of parameters.");
break;
}
{
Date modificationTime = this.ftplett.getModificationTime(argument);
if (modificationTime == null) {
this.sendReply(
"553 The system cannot determine the modification time of the file specified."
);
break;
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss.SSS");
sdf.setTimeZone(TimeZone.getTimeZone("GMT+0"));
this.sendReply("213 " + sdf.format(modificationTime));
}
break;
}
}
} finally {
ServerSocket pdcss = this.passiveDataConnectionServerSocket;
if (pdcss != null) {
try { pdcss.close(); } catch (Exception e) {}
}
}
}
// CONFIGURATION
private static final Logger LOGGER = Logger.getLogger(FtpSession.class.getName());
/** See RFC959, section 3.4 "TRANSMISSION MODES" */
private enum TransmissionMode { STREAM, BLOCK, COMPRESSED }
private final ProducerWhichThrows controlIn;
private final ConsumerWhichThrows controlOut;
private final InetAddress interfacE;
private final Ftplett ftplett;
// STATE
private boolean passiveDataTransfer;
@Nullable private InetSocketAddress activeDataRemoteSocketAddress;
@Nullable private ServerSocket passiveDataConnectionServerSocket;
private TransmissionMode transmissionMode = TransmissionMode.STREAM;
@Nullable private String renameFrom; // For RNFR / RNTO
// IMPLEMENTATION
/**
* Establishes the data connection, calls the {@code action}, then closes the data connection.
*/
private void
dataConnection(ConsumerWhichThrows action) throws IOException {
// Establish the data connection.
Socket dataSocket;
if (this.passiveDataTransfer) {
ServerSocket pdcss = this.passiveDataConnectionServerSocket;
assert pdcss != null;
LOGGER.fine("Accepting data connection on '" + pdcss.getLocalSocketAddress() + "'");
dataSocket = pdcss.accept();
} else {
InetSocketAddress adrsa = this.activeDataRemoteSocketAddress;
assert adrsa != null;
LOGGER.fine("Creating data connection to '" + adrsa + "'");
dataSocket = new Socket(adrsa.getAddress(), adrsa.getPort());
}
if (this.transmissionMode != TransmissionMode.STREAM) { // TODO Transmission modes other than STREAM
throw new IOException("Transmission mode '" + this.transmissionMode + "' NYI");
}
this.sendReply("125 Data connection created; Transfer starting.");
try {
// Execute the action on the data connection.
action.consume(dataSocket);
// Close the data connection.
LOGGER.fine("Closing data connection");
dataSocket.close();
} finally {
try { dataSocket.close(); } catch (Exception e) {}
}
this.sendReply("226 Transfer complete.");
}
class Command {
/** The command's code. */
CommandCode code;
/** The command's optional argument. */
@Nullable String argument;
public
Command(CommandCode code, @Nullable String argument) {
this.code = code;
this.argument = argument;
}
}
/**
* Wait for a valid command.
*
* @return {@code null} if the control connection signals end-of-input
*/
@Nullable private Command
receiveCommand() throws IOException {
for (;;) {
String line = this.controlIn.produce();
if (line == null) {
LOGGER.fine("Socket end-of-input");
return null;
}
LOGGER.fine("<<< " + line);
int idx = line.indexOf(' ');
String code, argument;
if (idx == -1) {
code = line;
argument = null;
} else {
code = line.substring(0, idx);
argument = line.substring(idx + 1);
}
try {
return new Command(CommandCode.valueOf(code.toUpperCase()), argument);
} catch (IllegalArgumentException iae) {
this.sendReply("500 '" + code + "': command not understood.");
}
}
}
/**
* Send a given reply.
*/
private void
sendReply(String line) throws IOException {
LOGGER.fine(">>> " + line);
this.controlOut.consume(line);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy