/*
Copyright 2009 David Revell
This file is part of SwiFTP.
SwiFTP is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
SwiFTP is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with SwiFTP. If not, see <http://www.gnu.org/licenses/>.
*/
package org.swiftp;
import net.micode.fileexplorer.FTPServerService;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import android.util.Log;
public class SessionThread extends Thread {
protected boolean shouldExit = false;
protected Socket cmdSocket;
protected MyLog myLog = new MyLog(getClass().getName());
protected ByteBuffer buffer = ByteBuffer.allocate(Defaults
.getInputBufferSize());
protected boolean pasvMode = false;
protected boolean binaryMode = false;
protected Account account = new Account();
protected boolean authenticated = false;
protected File workingDir = Globals.getChrootDir();
// protected ServerSocket dataServerSocket = null;
protected Socket dataSocket = null;
// protected FTPServerService service;
protected File renameFrom = null;
// protected InetAddress outDataDest = null;
// protected int outDataPort = 20; // 20 is the default ftp-data port
protected DataSocketFactory dataSocketFactory;
OutputStream dataOutputStream = null;
private boolean sendWelcomeBanner;
protected String encoding = Defaults.SESSION_ENCODING;
protected Source source;
int authFails = 0;
public enum Source {LOCAL, PROXY}; // where did this connection come from?
public static int MAX_AUTH_FAILS = 3;
/**
* Used when we get a PORT command to open up an outgoing socket.
*
* @return
*/
// public void setPortSocket(InetAddress dest, int port) {
// myLog.l(Log.DEBUG, "Setting PORT dest to " +
// dest.getHostAddress() + " port " + port);
// outDataDest = dest;
// outDataPort = port;
// }
/**
* Sends a string over the already-established data socket
*
* @param string
* @return Whether the send completed successfully
*/
public boolean sendViaDataSocket(String string) {
try {
byte[] bytes = string.getBytes(encoding);
myLog.d("Using data connection encoding: " + encoding);
return sendViaDataSocket(bytes, bytes.length);
} catch (UnsupportedEncodingException e) {
myLog.l(Log.ERROR, "Unsupported encoding for data socket send");
return false;
}
}
public boolean sendViaDataSocket(byte[] bytes, int len) {
return sendViaDataSocket(bytes, 0, len);
}
/**
* Sends a byte array over the already-established data socket
*
* @param bytes
* @param len
* @return
*/
public boolean sendViaDataSocket(byte[] bytes, int start, int len) {
if (dataOutputStream == null) {
myLog.l(Log.INFO, "Can't send via null dataOutputStream");
return false;
}
if (len == 0) {
return true; // this isn't an "error"
}
try {
dataOutputStream.write(bytes, start, len);
} catch (IOException e) {
myLog.l(Log.INFO, "Couldn't write output stream for data socket");
myLog.l(Log.INFO, e.toString());
return false;
}
dataSocketFactory.reportTraffic(len);
return true;
}
/**
* Received some bytes from the data socket, which is assumed to already be
* connected. The bytes are placed in the given array, and the number of
* bytes successfully read is returned.
*
* @param bytes
* Where to place the input bytes
* @return >0 if successful which is the number of bytes read, -1 if no
* bytes remain to be read, -2 if the data socket was not connected,
* 0 if there was a read error
*/
public int receiveFromDataSocket(byte[] buf) {
int bytesRead;
if (dataSocket == null) {
myLog.l(Log.INFO, "Can't receive from null dataSocket");
return -2;
}
if (!dataSocket.isConnected()) {
myLog.l(Log.INFO, "Can't receive from unconnected socket");
return -2;
}
InputStream in;
try {
in = dataSocket.getInputStream();
// If the read returns 0 bytes, the stream is not yet
// closed, but we just want to read again.
while ((bytesRead = in.read(buf, 0, buf.length)) == 0) {
}
if (bytesRead == -1) {
// If InputStream.read returns -1, there are no bytes
// remaining, so we return 0.
return -1;
}
} catch (IOException e) {
myLog.l(Log.INFO, "Error reading data socket");
return 0;
}
dataSocketFactory.reportTraffic(bytesRead);
return bytesRead;
}
/**
* Called when we receive a PASV command.
*
* @return Whether the necessary initialization was successful.
*/
public int onPasv() {
return dataSocketFactory.onPasv();
}
/**
* Called when we receive a PORT command.
*
* @return Whether the necessary initialization was successful.
*/
public boolean onPort(InetAddress dest, int port) {
return dataSocketFactory.onPort(dest, port);
}
public InetAddress getDataSocketPasvIp() {
// When the client sends PASV, our reply will contain the address and port
// of the data connection that the client should connect to. For this purpose
// we always use the same IP address that the command socket is using.
return cmdSocket.getLocalAddress();
// The old code, not totally correct.
// return dataSocketFactory.getPasvIp();
}
// public int getDataSocketPort() {
// return dataSocketFactory.getPortNumber();
// }
/**
* Will be called by (e.g.) CmdSTOR, CmdRETR, CmdLIST, etc. when they are
* about to start actually doing IO over the data socket.
*
* @return
*/
public boolean startUsingDataSocket() {
try {
dataSocket = dataSocketFactory.onTransfer();
if (dataSocket == null) {
myLog.l(Log.INFO,
"dataSocketFactory.onTransfer() returned null");
return false;
}
dataOutputStream = dataSocket.getOutputStream();
return true;
} catch (IOException e) {
myLog.l(Log.INFO,
"IOException getting OutputStream for data socket");
dataSocket = null;
return false;
}
}
public void quit() {
myLog.d("SessionThread told to quit");
closeSocket();
}
public void closeDataSocket() {
myLog.l(Log.DEBUG, "Closing data socket");
if (dataOutputStream != null) {
try {
dataOutputStream.close();
} catch (IOException e) {
}
dataOutputStream = null;
}
if (dataSocket != null) {
try {
dataSocket.close();
} catch (IOException e) {
}
}
dataSocket = null;
}
protected InetAddress getLocalAddress() {
return cmdSocket.getLocalAddress();
}
static int numNulls = 0;
public void run() {
myLog.l(Log.INFO, "SessionThread started");
if(sendWelcomeBanner) {
writeString("220 SwiFTP " + Util.getVersion() + " ready\r\n");
}
// Main loop: read an incoming line and process it
try {
BufferedReader in = new BufferedReader(new InputStreamReader(cmdSocket
.getInputStream()), 8192); // use 8k buffer
while (true) {
String line;
line = in.readLine(); // will accept \r\n or \n for terminator
if (line != null) {
FTPServerService.writeMonitor(true, line);
myLog.l(Log.DEBUG, "Received line from client: " + line);
FtpCmd.dispatchCommand(this, line);
} else {
myLog.i("readLine gave null, quitting");
break;
}
}
} catch (IOException e) {
myLog.l(Log.INFO, "Connection was dropped");
}
closeSocket();
}
/**
* A static method to check the equality of two byte arrays, but only up to
* a given length.
*/
public static boolean compareLen(byte[] array1, byte[] array2, int len) {
for (int i = 0; i < len; i++) {
if (array1[i] != array2[i]) {
return false;
}
}
return true;
}
public void closeSocket() {
if (cmdSocket == null) {
return;
}
try {
cmdSocket.close();
} catch (IOException e) {}
}
public void writeBytes(byte[] bytes) {
try {
// TODO: do we really want to do all of this on each write? Why?
BufferedOutputStream out = new BufferedOutputStream(cmdSocket
.getOutputStream(), Defaults.dataChunkSize);
out.write(bytes);
out.flush();
dataSocketFactory.reportTraffic(bytes.length);
} catch (IOException e) {
myLog.l(Log.INFO, "Exception writing socket");
closeSocket();
return;
}
}
public void writeString(String str) {
FTPServerService.writeMonitor(false, str);
byte[] strBytes;
try {
strBytes = str.getBytes(encoding);
} catch (UnsupportedEncodingException e) {
myLog.e("Unsupported encoding: " + encoding);
strBytes = str.getBytes();
}
writeBytes(strBytes);
}
protected Socket getSocket() {
return cmdSocket;
}
public Account getAccount() {
return account;
}
public void setAccount(Account account) {
this.account = account;
}
public boolean isPasvMode() {
return pasvMode;
}
public SessionThread(Socket socket, DataSocketFactory dataSocketFactory,
Source source) {
this.cmdSocket = socket;
this.source = source;
this.dataSocketFactory = dataSocketFactory;
if(source == Source.LOCAL) {
this.sendWelcomeBanner = true;
} else {
this.sendWelcomeBanner = false;
}
}
static public ByteBuffer stringToBB(String s) {
return ByteBuffer.wrap(s.getBytes());
}
public boolean isBinaryMode() {
return binaryMode;
}
public void setBinaryMode(boolean binaryMode) {
this.binaryMode = binaryMode;
}
public boolean isAuthenticated() {
return authenticated;
}
public void authAttempt(boolean authenticated) {
if (authenticated) {
myLog.l(Log.INFO, "Authentication complete");
this.authenticated = true;
} else {
// There was a failed auth attempt. If the connection came
// via the proxy, then drop it now. The client can't try again
// successfully because it doesn't know its real username. What
// it knows is prefix_username.
if(source == Source.PROXY) {
quit();
} else {
authFails++;
myLog.i("Auth failed: " + authFails + "/" + MAX_AUTH_FAILS);
}
if(authFails > MAX_AUTH_FAILS) {
myLog.i("Too many auth fails, quitting session");
quit();
}
}
}
public File getWorkingDir() {
return workingDir;
}
public void setWorkingDir(File workingDir) {
try {
this.workingDir = workingDir.getCanonicalFile().getAbsoluteFile();
} catch (IOException e) {
myLog.l(Log.INFO, "SessionThread canonical error");
}
}
/*
* public FTPServerService getService() { return service; }
*
* public void setService(FTPServerService service) { this.service =
* service; }
*/
public Socket getDataSocket() {
return dataSocket;
}
public void setDataSocket(Socket dataSocket) {
this.dataSocket = dataSocket;
}
// public ServerSocket getServerSocket() {
// return dataServerSocket;
// }
public File getRenameFrom() {
return renameFrom;
}
public void setRenameFrom(File renameFrom) {
this.renameFrom = renameFrom;
}
public String getEncoding() {
return encoding;
}
public void setEncoding(String encoding) {
this.encoding = encoding;
}
}