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 isAuthenticated = false;
|
||||||
private boolean isRunning = true;
|
private boolean isRunning = true;
|
||||||
private FTPFileSystem fileSystem;
|
private FTPFileSystem fileSystem;
|
||||||
|
private FTPDataConnection dataConnection;
|
||||||
|
private String transferType = "A"; // A = ASCII, I = Binary
|
||||||
|
|
||||||
public FTPSession(Socket socket) {
|
public FTPSession(Socket socket) {
|
||||||
this.controlSocket = socket;
|
this.controlSocket = socket;
|
||||||
this.fileSystem = new FTPFileSystem();
|
this.fileSystem = new FTPFileSystem();
|
||||||
|
this.dataConnection = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -98,6 +101,12 @@ public class FTPSession implements Runnable {
|
|||||||
case "SIZE":
|
case "SIZE":
|
||||||
handleSize(argument);
|
handleSize(argument);
|
||||||
break;
|
break;
|
||||||
|
case "TYPE":
|
||||||
|
handleType(argument);
|
||||||
|
break;
|
||||||
|
case "PASV":
|
||||||
|
handlePasv();
|
||||||
|
break;
|
||||||
case "NOOP":
|
case "NOOP":
|
||||||
handleNoop();
|
handleNoop();
|
||||||
break;
|
break;
|
||||||
@@ -179,14 +188,29 @@ public class FTPSession implements Runnable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For now, send file list directly without data connection
|
if (dataConnection == null) {
|
||||||
// Phase 4 will implement proper data connection
|
sendResponse(FTPResponse.CANNOT_OPEN_DATA_CONNECTION, "Use PASV first");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String fileList = fileSystem.formatFileList(true);
|
String fileList = fileSystem.formatFileList(true);
|
||||||
|
|
||||||
sendResponse(FTPResponse.FILE_STATUS_OK, "File list follows (inline, data connection not yet implemented)");
|
sendResponse(FTPResponse.FILE_STATUS_OK, "Opening data connection for directory list");
|
||||||
writer.write(fileList);
|
|
||||||
writer.flush();
|
if (dataConnection.acceptConnection()) {
|
||||||
|
if (dataConnection.sendData(fileList)) {
|
||||||
|
dataConnection.close();
|
||||||
sendResponse(FTPResponse.CLOSING_DATA_CONNECTION, "Directory send OK");
|
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 {
|
private void handleNlst(String path) throws IOException {
|
||||||
@@ -195,13 +219,29 @@ public class FTPSession implements Runnable {
|
|||||||
return;
|
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);
|
String fileList = fileSystem.formatFileList(false);
|
||||||
|
|
||||||
sendResponse(FTPResponse.FILE_STATUS_OK, "File list follows (inline, data connection not yet implemented)");
|
sendResponse(FTPResponse.FILE_STATUS_OK, "Opening data connection for name list");
|
||||||
writer.write(fileList);
|
|
||||||
writer.flush();
|
if (dataConnection.acceptConnection()) {
|
||||||
sendResponse(FTPResponse.CLOSING_DATA_CONNECTION, "Directory send OK");
|
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 {
|
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 {
|
private void handleNoop() throws IOException {
|
||||||
sendResponse(FTPResponse.COMMAND_OK, "OK");
|
sendResponse(FTPResponse.COMMAND_OK, "OK");
|
||||||
}
|
}
|
||||||
@@ -291,6 +382,9 @@ public class FTPSession implements Runnable {
|
|||||||
|
|
||||||
private void closeSession() {
|
private void closeSession() {
|
||||||
try {
|
try {
|
||||||
|
if (dataConnection != null) {
|
||||||
|
dataConnection.close();
|
||||||
|
}
|
||||||
if (reader != null) reader.close();
|
if (reader != null) reader.close();
|
||||||
if (writer != null) writer.close();
|
if (writer != null) writer.close();
|
||||||
if (controlSocket != null && !controlSocket.isClosed()) {
|
if (controlSocket != null && !controlSocket.isClosed()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user