From 3357fe76d4642c51225405ec3dab8190fcfb6a07 Mon Sep 17 00:00:00 2001 From: Gyubin Han Date: Fri, 2 Jan 2026 04:33:53 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=8F=AC?= =?UTF-8?q?=ED=8A=B8=20=EB=B2=94=EC=9C=84=20=EC=A7=80=EC=A0=95=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 방화벽 환경에서 특정 포트 범위만 개방하여 사용할 수 있도록 데이터 포트 범위 설정 기능 추가. - FTPConfig에 minDataPort, maxDataPort 설정 추가 (기본값: 50000-50100) - FTPDataConnection에서 지정된 범위 내 사용 가능한 포트 자동 할당 - MainActivity UI에 데이터 포트 범위 설정 필드 추가 - 포트 범위 유효성 검증 (최소 10개 포트, 1024-65535 범위) - 범위 미지정 시 기존처럼 랜덤 포트 사용 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../be/gyu/android/server/ftp/FTPConfig.java | 27 +++++++++ .../android/server/ftp/FTPDataConnection.java | 41 ++++++++++--- .../be/gyu/android/server/ftp/FTPServer.java | 14 ++++- .../be/gyu/android/server/ftp/FTPService.java | 17 ++++-- .../be/gyu/android/server/ftp/FTPSession.java | 12 +++- .../gyu/android/server/ftp/MainActivity.java | 49 ++++++++++++++++ app/src/main/res/layout/activity_main.xml | 58 ++++++++++++++++++- 7 files changed, 200 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/be/gyu/android/server/ftp/FTPConfig.java b/app/src/main/java/be/gyu/android/server/ftp/FTPConfig.java index 865fc67..a677993 100644 --- a/app/src/main/java/be/gyu/android/server/ftp/FTPConfig.java +++ b/app/src/main/java/be/gyu/android/server/ftp/FTPConfig.java @@ -9,8 +9,12 @@ public class FTPConfig { private static final String KEY_PORT = "ftp_port"; private static final String KEY_ROOT_DIR_URI = "root_directory_uri"; private static final String KEY_ROOT_DIR_PATH = "root_directory_path"; + private static final String KEY_MIN_DATA_PORT = "min_data_port"; + private static final String KEY_MAX_DATA_PORT = "max_data_port"; private static final int DEFAULT_PORT = 2121; + private static final int DEFAULT_MIN_DATA_PORT = 50000; + private static final int DEFAULT_MAX_DATA_PORT = 50100; private final SharedPreferences preferences; @@ -60,4 +64,27 @@ public class FTPConfig { .putString(KEY_ROOT_DIR_PATH, rootDirPath) .apply(); } + + public int getMinDataPort() { + return preferences.getInt(KEY_MIN_DATA_PORT, DEFAULT_MIN_DATA_PORT); + } + + public void setMinDataPort(int port) { + preferences.edit().putInt(KEY_MIN_DATA_PORT, port).apply(); + } + + public int getMaxDataPort() { + return preferences.getInt(KEY_MAX_DATA_PORT, DEFAULT_MAX_DATA_PORT); + } + + public void setMaxDataPort(int port) { + preferences.edit().putInt(KEY_MAX_DATA_PORT, port).apply(); + } + + public void setDataPortRange(int minPort, int maxPort) { + preferences.edit() + .putInt(KEY_MIN_DATA_PORT, minPort) + .putInt(KEY_MAX_DATA_PORT, maxPort) + .apply(); + } } diff --git a/app/src/main/java/be/gyu/android/server/ftp/FTPDataConnection.java b/app/src/main/java/be/gyu/android/server/ftp/FTPDataConnection.java index 92d7cf6..638ef2b 100644 --- a/app/src/main/java/be/gyu/android/server/ftp/FTPDataConnection.java +++ b/app/src/main/java/be/gyu/android/server/ftp/FTPDataConnection.java @@ -20,14 +20,41 @@ public class FTPDataConnection { 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(); + return openPassiveMode(bindAddress, 0, 0); + } - Log.i(TAG, "Passive mode enabled on port: " + passivePort); - return true; + public boolean openPassiveMode(InetAddress bindAddress, int minPort, int maxPort) { + try { + if (minPort <= 0 || maxPort <= 0 || minPort > maxPort) { + // 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 random port: " + passivePort); + return true; + } + + // Try to find an available port in the specified range + IOException lastException = null; + for (int port = minPort; port <= maxPort; port++) { + try { + passiveSocket = new ServerSocket(port, 1, bindAddress); + passiveSocket.setSoTimeout(DATA_CONNECTION_TIMEOUT); + passivePort = passiveSocket.getLocalPort(); + Log.i(TAG, "Passive mode enabled on port: " + passivePort + " (range: " + minPort + "-" + maxPort + ")"); + return true; + } catch (IOException e) { + lastException = e; + // Port is in use, try next port + } + } + + // No available port found in range + Log.e(TAG, "No available port in range " + minPort + "-" + maxPort); + if (lastException != null) { + Log.e(TAG, "Last error: " + lastException.getMessage()); + } + return false; } catch (IOException e) { Log.e(TAG, "Error opening passive mode: " + e.getMessage()); return false; diff --git a/app/src/main/java/be/gyu/android/server/ftp/FTPServer.java b/app/src/main/java/be/gyu/android/server/ftp/FTPServer.java index 354541c..825aaa5 100644 --- a/app/src/main/java/be/gyu/android/server/ftp/FTPServer.java +++ b/app/src/main/java/be/gyu/android/server/ftp/FTPServer.java @@ -23,19 +23,27 @@ public class FTPServer { private int port; private Context context; private Uri rootDirectoryUri; + private int minDataPort = 0; + private int maxDataPort = 0; public FTPServer(Context context) { - this(context, DEFAULT_PORT, null); + this(context, DEFAULT_PORT, null, 0, 0); } public FTPServer(Context context, int port) { - this(context, port, null); + this(context, port, null, 0, 0); } public FTPServer(Context context, int port, Uri rootDirectoryUri) { + this(context, port, rootDirectoryUri, 0, 0); + } + + public FTPServer(Context context, int port, Uri rootDirectoryUri, int minDataPort, int maxDataPort) { this.context = context; this.port = port; this.rootDirectoryUri = rootDirectoryUri; + this.minDataPort = minDataPort; + this.maxDataPort = maxDataPort; this.executorService = Executors.newFixedThreadPool(MAX_CONNECTIONS); } @@ -56,7 +64,7 @@ public class FTPServer { Socket clientSocket = serverSocket.accept(); Log.i(TAG, "New client connection from: " + clientSocket.getInetAddress()); - FTPSession session = new FTPSession(clientSocket, context, rootDirectoryUri); + FTPSession session = new FTPSession(clientSocket, context, rootDirectoryUri, minDataPort, maxDataPort); executorService.execute(session); } catch (IOException e) { diff --git a/app/src/main/java/be/gyu/android/server/ftp/FTPService.java b/app/src/main/java/be/gyu/android/server/ftp/FTPService.java index 0a9c2d7..e942bae 100644 --- a/app/src/main/java/be/gyu/android/server/ftp/FTPService.java +++ b/app/src/main/java/be/gyu/android/server/ftp/FTPService.java @@ -37,11 +37,13 @@ public class FTPService extends Service { if (ACTION_START.equals(action)) { int port = intent.getIntExtra("port", 2121); String rootDirUriString = intent.getStringExtra("rootDirUri"); + int minDataPort = intent.getIntExtra("minDataPort", 0); + int maxDataPort = intent.getIntExtra("maxDataPort", 0); android.net.Uri rootDirUri = null; if (rootDirUriString != null && !rootDirUriString.isEmpty()) { rootDirUri = android.net.Uri.parse(rootDirUriString); } - startFTPServer(port, rootDirUri); + startFTPServer(port, rootDirUri, minDataPort, maxDataPort); } else if (ACTION_STOP.equals(action)) { stopFTPServer(); } @@ -50,19 +52,24 @@ public class FTPService extends Service { return START_STICKY; } - private void startFTPServer(int port, android.net.Uri rootDirUri) { + private void startFTPServer(int port, android.net.Uri rootDirUri, int minDataPort, int maxDataPort) { if (ftpServer != null && ftpServer.isRunning()) { Log.w(TAG, "FTP Server is already running"); return; } - ftpServer = new FTPServer(this, port, rootDirUri); + ftpServer = new FTPServer(this, port, rootDirUri, minDataPort, maxDataPort); ftpServer.start(); - Notification notification = createNotification("FTP Server is running on port " + ftpServer.getPort()); + String notificationText = "FTP Server is running on port " + ftpServer.getPort(); + if (minDataPort > 0 && maxDataPort > 0) { + notificationText += " (Data ports: " + minDataPort + "-" + maxDataPort + ")"; + } + Notification notification = createNotification(notificationText); startForeground(NOTIFICATION_ID, notification); - Log.i(TAG, "FTP Server started on port " + port + " with root URI: " + rootDirUri); + Log.i(TAG, "FTP Server started on port " + port + " with root URI: " + rootDirUri + + ", data port range: " + minDataPort + "-" + maxDataPort); } private void stopFTPServer() { diff --git a/app/src/main/java/be/gyu/android/server/ftp/FTPSession.java b/app/src/main/java/be/gyu/android/server/ftp/FTPSession.java index e7e0af1..8630dcb 100644 --- a/app/src/main/java/be/gyu/android/server/ftp/FTPSession.java +++ b/app/src/main/java/be/gyu/android/server/ftp/FTPSession.java @@ -31,15 +31,23 @@ public class FTPSession implements Runnable { private FTPDataConnection dataConnection; private String transferType = "A"; // A = ASCII, I = Binary private boolean useUtf8 = true; // UTF-8 enabled by default for better compatibility + private int minDataPort = 0; + private int maxDataPort = 0; public FTPSession(Socket socket, Context context) { - this(socket, context, null); + this(socket, context, null, 0, 0); } public FTPSession(Socket socket, Context context, Uri rootDirectoryUri) { + this(socket, context, rootDirectoryUri, 0, 0); + } + + public FTPSession(Socket socket, Context context, Uri rootDirectoryUri, int minDataPort, int maxDataPort) { this.controlSocket = socket; this.fileSystem = new FTPFileSystem(context, rootDirectoryUri); this.dataConnection = null; + this.minDataPort = minDataPort; + this.maxDataPort = maxDataPort; } @Override @@ -405,7 +413,7 @@ public class FTPSession implements Runnable { // Get server address from control socket String serverAddress = controlSocket.getLocalAddress().getHostAddress(); - if (dataConnection.openPassiveMode(controlSocket.getLocalAddress())) { + if (dataConnection.openPassiveMode(controlSocket.getLocalAddress(), minDataPort, maxDataPort)) { int port = dataConnection.getPassivePort(); // Format: h1,h2,h3,h4,p1,p2 diff --git a/app/src/main/java/be/gyu/android/server/ftp/MainActivity.java b/app/src/main/java/be/gyu/android/server/ftp/MainActivity.java index c30b11f..9d15e25 100644 --- a/app/src/main/java/be/gyu/android/server/ftp/MainActivity.java +++ b/app/src/main/java/be/gyu/android/server/ftp/MainActivity.java @@ -35,6 +35,8 @@ public class MainActivity extends AppCompatActivity { private TextView rootDirPathTextView; private Button selectDirButton; private Button saveSettingsButton; + private EditText minDataPortEditText; + private EditText maxDataPortEditText; private FTPConfig config; private ActivityResultLauncher directoryPickerLauncher; @@ -91,11 +93,15 @@ public class MainActivity extends AppCompatActivity { rootDirPathTextView = findViewById(R.id.rootDirPathTextView); selectDirButton = findViewById(R.id.selectDirButton); saveSettingsButton = findViewById(R.id.saveSettingsButton); + minDataPortEditText = findViewById(R.id.minDataPortEditText); + maxDataPortEditText = findViewById(R.id.maxDataPortEditText); } private void loadSettings() { int port = config.getPort(); String rootPath = config.getRootDirectoryPath(); + int minDataPort = config.getMinDataPort(); + int maxDataPort = config.getMaxDataPort(); portEditText.setText(String.valueOf(port)); portTextView.setText("Port: " + port); @@ -105,6 +111,9 @@ public class MainActivity extends AppCompatActivity { } else { rootDirPathTextView.setText("Not selected"); } + + minDataPortEditText.setText(String.valueOf(minDataPort)); + maxDataPortEditText.setText(String.valueOf(maxDataPort)); } private void setupListeners() { @@ -142,7 +151,45 @@ public class MainActivity extends AppCompatActivity { return; } + // Validate data port range + String minPortStr = minDataPortEditText.getText().toString().trim(); + String maxPortStr = maxDataPortEditText.getText().toString().trim(); + + int minDataPort = 0; + int maxDataPort = 0; + + if (!minPortStr.isEmpty() || !maxPortStr.isEmpty()) { + if (minPortStr.isEmpty() || maxPortStr.isEmpty()) { + Toast.makeText(this, "Please enter both min and max data ports or leave both empty", Toast.LENGTH_SHORT).show(); + return; + } + + try { + minDataPort = Integer.parseInt(minPortStr); + maxDataPort = Integer.parseInt(maxPortStr); + + if (minDataPort < 1024 || minDataPort > 65535 || maxDataPort < 1024 || maxDataPort > 65535) { + Toast.makeText(this, "Data ports must be between 1024 and 65535", Toast.LENGTH_SHORT).show(); + return; + } + + if (minDataPort >= maxDataPort) { + Toast.makeText(this, "Min data port must be less than max data port", Toast.LENGTH_SHORT).show(); + return; + } + + if (maxDataPort - minDataPort < 10) { + Toast.makeText(this, "Data port range should be at least 10 ports", Toast.LENGTH_SHORT).show(); + return; + } + } catch (NumberFormatException e) { + Toast.makeText(this, "Invalid data port number", Toast.LENGTH_SHORT).show(); + return; + } + } + config.setPort(port); + config.setDataPortRange(minDataPort, maxDataPort); portTextView.setText("Port: " + port); Toast.makeText(this, "Settings saved", Toast.LENGTH_SHORT).show(); @@ -196,6 +243,8 @@ public class MainActivity extends AppCompatActivity { serviceIntent.setAction(FTPService.ACTION_START); serviceIntent.putExtra("port", config.getPort()); serviceIntent.putExtra("rootDirUri", rootDirUri); + serviceIntent.putExtra("minDataPort", config.getMinDataPort()); + serviceIntent.putExtra("maxDataPort", config.getMaxDataPort()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForegroundService(serviceIntent); diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 6351d7d..97f9a68 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -144,6 +144,62 @@ app:layout_constraintTop_toBottomOf="@id/rootDirPathTextView" app:layout_constraintStart_toStartOf="parent" /> + + + + + + + + + +