Feat: 데이터 연결 및 Passive Mode 구현
- FTPDataConnection 클래스 생성: 데이터 연결 관리 * Passive Mode를 위한 ServerSocket 생성 * 동적 포트 할당 및 클라이언트 연결 수락 * 데이터 송수신 기능 (바이트 배열, 문자열, 스트림) * 30초 타임아웃 설정 - FTPSession에 데이터 연결 통합 * dataConnection 필드 및 transferType 필드 추가 * 세션 종료 시 데이터 연결 자동 닫기 - PASV 명령어 구현 * Passive Mode 진입 * 서버 주소와 포트를 h1,h2,h3,h4,p1,p2 형식으로 응답 * 이전 데이터 연결 자동 종료 - TYPE 명령어 구현 * ASCII (A) 및 Binary (I) 모드 지원 * 전송 타입 상태 관리 - LIST/NLST 명령어를 데이터 연결로 수정 * PASV 명령어 선행 필수 * 데이터 연결을 통해 파일 목록 전송 * 전송 완료 후 연결 자동 종료 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
package be.gyu.android.server.ftp;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
|
||||
public class FTPDataConnection {
|
||||
private static final String TAG = "FTPDataConnection";
|
||||
private static final int DATA_CONNECTION_TIMEOUT = 30000; // 30 seconds
|
||||
|
||||
private ServerSocket passiveSocket;
|
||||
private Socket dataSocket;
|
||||
private int passivePort;
|
||||
|
||||
public boolean openPassiveMode(InetAddress bindAddress) {
|
||||
try {
|
||||
// Use port 0 to get a random available port
|
||||
passiveSocket = new ServerSocket(0, 1, bindAddress);
|
||||
passiveSocket.setSoTimeout(DATA_CONNECTION_TIMEOUT);
|
||||
passivePort = passiveSocket.getLocalPort();
|
||||
|
||||
Log.i(TAG, "Passive mode enabled on port: " + passivePort);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error opening passive mode: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean acceptConnection() {
|
||||
if (passiveSocket == null) {
|
||||
Log.e(TAG, "Passive socket not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
Log.d(TAG, "Waiting for data connection...");
|
||||
dataSocket = passiveSocket.accept();
|
||||
Log.i(TAG, "Data connection established from: " + dataSocket.getInetAddress());
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error accepting data connection: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean sendData(byte[] data) {
|
||||
if (dataSocket == null || dataSocket.isClosed()) {
|
||||
Log.e(TAG, "Data socket not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
OutputStream out = new BufferedOutputStream(dataSocket.getOutputStream());
|
||||
out.write(data);
|
||||
out.flush();
|
||||
Log.d(TAG, "Sent " + data.length + " bytes");
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error sending data: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean sendData(String text) {
|
||||
return sendData(text.getBytes());
|
||||
}
|
||||
|
||||
public byte[] receiveData(int maxSize) {
|
||||
if (dataSocket == null || dataSocket.isClosed()) {
|
||||
Log.e(TAG, "Data socket not available");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
InputStream in = new BufferedInputStream(dataSocket.getInputStream());
|
||||
byte[] buffer = new byte[maxSize];
|
||||
int bytesRead = in.read(buffer);
|
||||
|
||||
if (bytesRead > 0) {
|
||||
byte[] data = new byte[bytesRead];
|
||||
System.arraycopy(buffer, 0, data, 0, bytesRead);
|
||||
Log.d(TAG, "Received " + bytesRead + " bytes");
|
||||
return data;
|
||||
} else {
|
||||
Log.w(TAG, "No data received");
|
||||
return null;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error receiving data: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean transferStream(InputStream inputStream) {
|
||||
if (dataSocket == null || dataSocket.isClosed()) {
|
||||
Log.e(TAG, "Data socket not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
OutputStream out = new BufferedOutputStream(dataSocket.getOutputStream());
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
long totalBytes = 0;
|
||||
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, bytesRead);
|
||||
totalBytes += bytesRead;
|
||||
}
|
||||
|
||||
out.flush();
|
||||
Log.i(TAG, "Transferred " + totalBytes + " bytes via stream");
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error transferring stream: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean receiveStream(OutputStream outputStream) {
|
||||
if (dataSocket == null || dataSocket.isClosed()) {
|
||||
Log.e(TAG, "Data socket not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
InputStream in = new BufferedInputStream(dataSocket.getInputStream());
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
long totalBytes = 0;
|
||||
|
||||
while ((bytesRead = in.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
totalBytes += bytesRead;
|
||||
}
|
||||
|
||||
outputStream.flush();
|
||||
Log.i(TAG, "Received " + totalBytes + " bytes via stream");
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error receiving stream: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
if (dataSocket != null && !dataSocket.isClosed()) {
|
||||
dataSocket.close();
|
||||
Log.d(TAG, "Data socket closed");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error closing data socket: " + e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
if (passiveSocket != null && !passiveSocket.isClosed()) {
|
||||
passiveSocket.close();
|
||||
Log.d(TAG, "Passive socket closed");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error closing passive socket: " + e.getMessage());
|
||||
}
|
||||
|
||||
dataSocket = null;
|
||||
passiveSocket = null;
|
||||
}
|
||||
|
||||
public int getPassivePort() {
|
||||
return passivePort;
|
||||
}
|
||||
|
||||
public boolean isDataConnectionOpen() {
|
||||
return dataSocket != null && !dataSocket.isClosed();
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,13 @@ public class FTPSession implements Runnable {
|
||||
private boolean isAuthenticated = false;
|
||||
private boolean isRunning = true;
|
||||
private FTPFileSystem fileSystem;
|
||||
private FTPDataConnection dataConnection;
|
||||
private String transferType = "A"; // A = ASCII, I = Binary
|
||||
|
||||
public FTPSession(Socket socket) {
|
||||
this.controlSocket = socket;
|
||||
this.fileSystem = new FTPFileSystem();
|
||||
this.dataConnection = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -98,6 +101,12 @@ public class FTPSession implements Runnable {
|
||||
case "SIZE":
|
||||
handleSize(argument);
|
||||
break;
|
||||
case "TYPE":
|
||||
handleType(argument);
|
||||
break;
|
||||
case "PASV":
|
||||
handlePasv();
|
||||
break;
|
||||
case "NOOP":
|
||||
handleNoop();
|
||||
break;
|
||||
@@ -179,14 +188,29 @@ public class FTPSession implements Runnable {
|
||||
return;
|
||||
}
|
||||
|
||||
// For now, send file list directly without data connection
|
||||
// Phase 4 will implement proper data connection
|
||||
if (dataConnection == null) {
|
||||
sendResponse(FTPResponse.CANNOT_OPEN_DATA_CONNECTION, "Use PASV first");
|
||||
return;
|
||||
}
|
||||
|
||||
String fileList = fileSystem.formatFileList(true);
|
||||
|
||||
sendResponse(FTPResponse.FILE_STATUS_OK, "File list follows (inline, data connection not yet implemented)");
|
||||
writer.write(fileList);
|
||||
writer.flush();
|
||||
sendResponse(FTPResponse.FILE_STATUS_OK, "Opening data connection for directory list");
|
||||
|
||||
if (dataConnection.acceptConnection()) {
|
||||
if (dataConnection.sendData(fileList)) {
|
||||
dataConnection.close();
|
||||
sendResponse(FTPResponse.CLOSING_DATA_CONNECTION, "Directory send OK");
|
||||
} else {
|
||||
dataConnection.close();
|
||||
sendResponse(FTPResponse.CONNECTION_CLOSED, "Transfer failed");
|
||||
}
|
||||
} else {
|
||||
dataConnection.close();
|
||||
sendResponse(FTPResponse.CANNOT_OPEN_DATA_CONNECTION, "Cannot open data connection");
|
||||
}
|
||||
|
||||
dataConnection = null;
|
||||
}
|
||||
|
||||
private void handleNlst(String path) throws IOException {
|
||||
@@ -195,13 +219,29 @@ public class FTPSession implements Runnable {
|
||||
return;
|
||||
}
|
||||
|
||||
// For now, send file list directly without data connection
|
||||
if (dataConnection == null) {
|
||||
sendResponse(FTPResponse.CANNOT_OPEN_DATA_CONNECTION, "Use PASV first");
|
||||
return;
|
||||
}
|
||||
|
||||
String fileList = fileSystem.formatFileList(false);
|
||||
|
||||
sendResponse(FTPResponse.FILE_STATUS_OK, "File list follows (inline, data connection not yet implemented)");
|
||||
writer.write(fileList);
|
||||
writer.flush();
|
||||
sendResponse(FTPResponse.CLOSING_DATA_CONNECTION, "Directory send OK");
|
||||
sendResponse(FTPResponse.FILE_STATUS_OK, "Opening data connection for name list");
|
||||
|
||||
if (dataConnection.acceptConnection()) {
|
||||
if (dataConnection.sendData(fileList)) {
|
||||
dataConnection.close();
|
||||
sendResponse(FTPResponse.CLOSING_DATA_CONNECTION, "Transfer complete");
|
||||
} else {
|
||||
dataConnection.close();
|
||||
sendResponse(FTPResponse.CONNECTION_CLOSED, "Transfer failed");
|
||||
}
|
||||
} else {
|
||||
dataConnection.close();
|
||||
sendResponse(FTPResponse.CANNOT_OPEN_DATA_CONNECTION, "Cannot open data connection");
|
||||
}
|
||||
|
||||
dataConnection = null;
|
||||
}
|
||||
|
||||
private void handleMkd(String dirName) throws IOException {
|
||||
@@ -278,6 +318,57 @@ public class FTPSession implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleType(String typeCode) throws IOException {
|
||||
if (typeCode.isEmpty()) {
|
||||
sendResponse(FTPResponse.SYNTAX_ERROR_PARAMETERS, "No type specified");
|
||||
return;
|
||||
}
|
||||
|
||||
String type = typeCode.toUpperCase();
|
||||
if (type.equals("A") || type.equals("I")) {
|
||||
transferType = type;
|
||||
String typeName = type.equals("A") ? "ASCII" : "Binary";
|
||||
sendResponse(FTPResponse.COMMAND_OK, "Type set to " + typeName);
|
||||
} else {
|
||||
sendResponse(FTPResponse.COMMAND_NOT_IMPLEMENTED_FOR_PARAMETER, "Type not supported");
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePasv() throws IOException {
|
||||
if (!isAuthenticated) {
|
||||
sendResponse(FTPResponse.NOT_LOGGED_IN, "Please login first");
|
||||
return;
|
||||
}
|
||||
|
||||
// Close previous data connection if exists
|
||||
if (dataConnection != null) {
|
||||
dataConnection.close();
|
||||
}
|
||||
|
||||
dataConnection = new FTPDataConnection();
|
||||
|
||||
// Get server address from control socket
|
||||
String serverAddress = controlSocket.getLocalAddress().getHostAddress();
|
||||
|
||||
if (dataConnection.openPassiveMode(controlSocket.getLocalAddress())) {
|
||||
int port = dataConnection.getPassivePort();
|
||||
|
||||
// Format: h1,h2,h3,h4,p1,p2
|
||||
// where IP is h1.h2.h3.h4 and port is p1*256+p2
|
||||
String[] addressParts = serverAddress.split("\\.");
|
||||
int p1 = port / 256;
|
||||
int p2 = port % 256;
|
||||
|
||||
String pasvResponse = String.format("Entering Passive Mode (%s,%s,%s,%s,%d,%d)",
|
||||
addressParts[0], addressParts[1], addressParts[2], addressParts[3], p1, p2);
|
||||
|
||||
sendResponse(FTPResponse.ENTERING_PASSIVE_MODE, pasvResponse);
|
||||
Log.i(TAG, "Passive mode: " + serverAddress + ":" + port);
|
||||
} else {
|
||||
sendResponse(FTPResponse.CANNOT_OPEN_DATA_CONNECTION, "Cannot open passive mode");
|
||||
}
|
||||
}
|
||||
|
||||
private void handleNoop() throws IOException {
|
||||
sendResponse(FTPResponse.COMMAND_OK, "OK");
|
||||
}
|
||||
@@ -291,6 +382,9 @@ public class FTPSession implements Runnable {
|
||||
|
||||
private void closeSession() {
|
||||
try {
|
||||
if (dataConnection != null) {
|
||||
dataConnection.close();
|
||||
}
|
||||
if (reader != null) reader.close();
|
||||
if (writer != null) writer.close();
|
||||
if (controlSocket != null && !controlSocket.isClosed()) {
|
||||
|
||||
Reference in New Issue
Block a user