diff --git a/app/src/main/java/be/gyu/android/server/ftp/FTPFileSystem.java b/app/src/main/java/be/gyu/android/server/ftp/FTPFileSystem.java new file mode 100644 index 0000000..70f84f3 --- /dev/null +++ b/app/src/main/java/be/gyu/android/server/ftp/FTPFileSystem.java @@ -0,0 +1,271 @@ +package be.gyu.android.server.ftp; + +import android.os.Environment; +import android.util.Log; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +public class FTPFileSystem { + private static final String TAG = "FTPFileSystem"; + + private final File rootDirectory; + private File currentDirectory; + + public FTPFileSystem() { + // Use external storage public directory as root + // In production, you might want to use app-specific directory or allow user to choose + File externalStorage = Environment.getExternalStorageDirectory(); + this.rootDirectory = new File(externalStorage, "FTPServer"); + + // Create root directory if it doesn't exist + if (!rootDirectory.exists()) { + if (rootDirectory.mkdirs()) { + Log.i(TAG, "Root directory created: " + rootDirectory.getAbsolutePath()); + } else { + Log.w(TAG, "Failed to create root directory, using external storage root"); + this.rootDirectory = externalStorage; + } + } + + this.currentDirectory = rootDirectory; + Log.d(TAG, "File system initialized. Root: " + rootDirectory.getAbsolutePath()); + } + + public String getCurrentPath() { + String relativePath = getRelativePath(currentDirectory); + return relativePath.isEmpty() ? "/" : "/" + relativePath; + } + + public boolean changeDirectory(String path) { + File newDir; + + if (path.startsWith("/")) { + // Absolute path + newDir = new File(rootDirectory, path.substring(1)); + } else { + // Relative path + newDir = new File(currentDirectory, path); + } + + try { + String canonicalPath = newDir.getCanonicalPath(); + String rootPath = rootDirectory.getCanonicalPath(); + + // Security check: prevent escaping root directory + if (!canonicalPath.startsWith(rootPath)) { + Log.w(TAG, "Attempted to escape root directory: " + canonicalPath); + return false; + } + + if (newDir.exists() && newDir.isDirectory()) { + currentDirectory = newDir; + Log.d(TAG, "Changed directory to: " + currentDirectory.getAbsolutePath()); + return true; + } else { + Log.w(TAG, "Directory does not exist: " + newDir.getAbsolutePath()); + return false; + } + } catch (Exception e) { + Log.e(TAG, "Error changing directory: " + e.getMessage()); + return false; + } + } + + public boolean changeToParentDirectory() { + File parent = currentDirectory.getParentFile(); + + if (parent == null) { + return false; + } + + try { + String parentPath = parent.getCanonicalPath(); + String rootPath = rootDirectory.getCanonicalPath(); + + // Cannot go above root directory + if (!parentPath.startsWith(rootPath)) { + return false; + } + + currentDirectory = parent; + Log.d(TAG, "Changed to parent directory: " + currentDirectory.getAbsolutePath()); + return true; + } catch (Exception e) { + Log.e(TAG, "Error changing to parent directory: " + e.getMessage()); + return false; + } + } + + public List listFiles() { + File[] files = currentDirectory.listFiles(); + List fileList = new ArrayList<>(); + + if (files != null) { + for (File file : files) { + fileList.add(file); + } + } + + return fileList; + } + + public String formatFileList(boolean detailed) { + List files = listFiles(); + StringBuilder sb = new StringBuilder(); + + if (detailed) { + // LIST format: Unix-style detailed listing + SimpleDateFormat dateFormat = new SimpleDateFormat("MMM dd HH:mm", Locale.US); + + for (File file : files) { + sb.append(formatDetailedFile(file, dateFormat)).append("\r\n"); + } + } else { + // NLST format: names only + for (File file : files) { + sb.append(file.getName()).append("\r\n"); + } + } + + return sb.toString(); + } + + private String formatDetailedFile(File file, SimpleDateFormat dateFormat) { + // Format: drwxrwxrwx 1 owner group size date name + String permissions = file.isDirectory() ? "drwxr-xr-x" : "-rw-r--r--"; + String owner = "ftp"; + String group = "ftp"; + long size = file.length(); + String date = dateFormat.format(new Date(file.lastModified())); + String name = file.getName(); + + return String.format("%s %4d %-8s %-8s %10d %s %s", + permissions, 1, owner, group, size, date, name); + } + + public boolean makeDirectory(String dirName) { + File newDir = new File(currentDirectory, dirName); + + try { + String canonicalPath = newDir.getCanonicalPath(); + String rootPath = rootDirectory.getCanonicalPath(); + + if (!canonicalPath.startsWith(rootPath)) { + return false; + } + + if (newDir.mkdir()) { + Log.i(TAG, "Directory created: " + newDir.getAbsolutePath()); + return true; + } else { + Log.w(TAG, "Failed to create directory: " + newDir.getAbsolutePath()); + return false; + } + } catch (Exception e) { + Log.e(TAG, "Error creating directory: " + e.getMessage()); + return false; + } + } + + public boolean removeDirectory(String dirName) { + File dir = new File(currentDirectory, dirName); + + try { + String canonicalPath = dir.getCanonicalPath(); + String rootPath = rootDirectory.getCanonicalPath(); + + if (!canonicalPath.startsWith(rootPath)) { + return false; + } + + if (dir.exists() && dir.isDirectory() && dir.delete()) { + Log.i(TAG, "Directory removed: " + dir.getAbsolutePath()); + return true; + } else { + Log.w(TAG, "Failed to remove directory: " + dir.getAbsolutePath()); + return false; + } + } catch (Exception e) { + Log.e(TAG, "Error removing directory: " + e.getMessage()); + return false; + } + } + + public boolean deleteFile(String fileName) { + File file = new File(currentDirectory, fileName); + + try { + String canonicalPath = file.getCanonicalPath(); + String rootPath = rootDirectory.getCanonicalPath(); + + if (!canonicalPath.startsWith(rootPath)) { + return false; + } + + if (file.exists() && file.isFile() && file.delete()) { + Log.i(TAG, "File deleted: " + file.getAbsolutePath()); + return true; + } else { + Log.w(TAG, "Failed to delete file: " + file.getAbsolutePath()); + return false; + } + } catch (Exception e) { + Log.e(TAG, "Error deleting file: " + e.getMessage()); + return false; + } + } + + public File getFile(String fileName) { + File file = new File(currentDirectory, fileName); + + try { + String canonicalPath = file.getCanonicalPath(); + String rootPath = rootDirectory.getCanonicalPath(); + + if (!canonicalPath.startsWith(rootPath)) { + return null; + } + + return file.exists() ? file : null; + } catch (Exception e) { + Log.e(TAG, "Error getting file: " + e.getMessage()); + return null; + } + } + + public long getFileSize(String fileName) { + File file = getFile(fileName); + return (file != null && file.isFile()) ? file.length() : -1; + } + + private String getRelativePath(File file) { + try { + String filePath = file.getCanonicalPath(); + String rootPath = rootDirectory.getCanonicalPath(); + + if (filePath.equals(rootPath)) { + return ""; + } else if (filePath.startsWith(rootPath + File.separator)) { + return filePath.substring(rootPath.length() + 1).replace(File.separator, "/"); + } else { + return ""; + } + } catch (Exception e) { + Log.e(TAG, "Error getting relative path: " + e.getMessage()); + return ""; + } + } + + public File getRootDirectory() { + return rootDirectory; + } + + public File getCurrentDirectory() { + return currentDirectory; + } +} 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 210e97b..6acfaa8 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 @@ -18,11 +18,12 @@ public class FTPSession implements Runnable { private String username; private boolean isAuthenticated = false; - private String currentDirectory = "/"; private boolean isRunning = true; + private FTPFileSystem fileSystem; public FTPSession(Socket socket) { this.controlSocket = socket; + this.fileSystem = new FTPFileSystem(); } @Override @@ -73,6 +74,30 @@ public class FTPSession implements Runnable { case "PWD": handlePwd(); break; + case "CWD": + handleCwd(argument); + break; + case "CDUP": + handleCdup(); + break; + case "LIST": + handleList(argument); + break; + case "NLST": + handleNlst(argument); + break; + case "MKD": + handleMkd(argument); + break; + case "RMD": + handleRmd(argument); + break; + case "DELE": + handleDele(argument); + break; + case "SIZE": + handleSize(argument); + break; case "NOOP": handleNoop(); break; @@ -113,7 +138,144 @@ public class FTPSession implements Runnable { sendResponse(FTPResponse.NOT_LOGGED_IN, "Please login first"); return; } - sendResponse(FTPResponse.PATHNAME_CREATED, "\"" + currentDirectory + "\" is current directory"); + String path = fileSystem.getCurrentPath(); + sendResponse(FTPResponse.PATHNAME_CREATED, "\"" + path + "\" is current directory"); + } + + private void handleCwd(String path) throws IOException { + if (!isAuthenticated) { + sendResponse(FTPResponse.NOT_LOGGED_IN, "Please login first"); + return; + } + + if (path.isEmpty()) { + sendResponse(FTPResponse.SYNTAX_ERROR_PARAMETERS, "No directory specified"); + return; + } + + if (fileSystem.changeDirectory(path)) { + sendResponse(FTPResponse.REQUESTED_FILE_ACTION_OK, "Directory changed to " + fileSystem.getCurrentPath()); + } else { + sendResponse(FTPResponse.FILE_UNAVAILABLE, "Failed to change directory"); + } + } + + private void handleCdup() throws IOException { + if (!isAuthenticated) { + sendResponse(FTPResponse.NOT_LOGGED_IN, "Please login first"); + return; + } + + if (fileSystem.changeToParentDirectory()) { + sendResponse(FTPResponse.REQUESTED_FILE_ACTION_OK, "Directory changed to " + fileSystem.getCurrentPath()); + } else { + sendResponse(FTPResponse.FILE_UNAVAILABLE, "Already at root directory"); + } + } + + private void handleList(String path) throws IOException { + if (!isAuthenticated) { + sendResponse(FTPResponse.NOT_LOGGED_IN, "Please login first"); + return; + } + + // For now, send file list directly without data connection + // Phase 4 will implement proper data connection + 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.CLOSING_DATA_CONNECTION, "Directory send OK"); + } + + private void handleNlst(String path) throws IOException { + if (!isAuthenticated) { + sendResponse(FTPResponse.NOT_LOGGED_IN, "Please login first"); + return; + } + + // For now, send file list directly without data connection + 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"); + } + + private void handleMkd(String dirName) throws IOException { + if (!isAuthenticated) { + sendResponse(FTPResponse.NOT_LOGGED_IN, "Please login first"); + return; + } + + if (dirName.isEmpty()) { + sendResponse(FTPResponse.SYNTAX_ERROR_PARAMETERS, "No directory name specified"); + return; + } + + if (fileSystem.makeDirectory(dirName)) { + String newPath = fileSystem.getCurrentPath() + "/" + dirName; + sendResponse(FTPResponse.PATHNAME_CREATED, "\"" + newPath + "\" directory created"); + } else { + sendResponse(FTPResponse.FILE_UNAVAILABLE, "Failed to create directory"); + } + } + + private void handleRmd(String dirName) throws IOException { + if (!isAuthenticated) { + sendResponse(FTPResponse.NOT_LOGGED_IN, "Please login first"); + return; + } + + if (dirName.isEmpty()) { + sendResponse(FTPResponse.SYNTAX_ERROR_PARAMETERS, "No directory name specified"); + return; + } + + if (fileSystem.removeDirectory(dirName)) { + sendResponse(FTPResponse.REQUESTED_FILE_ACTION_OK, "Directory removed"); + } else { + sendResponse(FTPResponse.FILE_UNAVAILABLE, "Failed to remove directory"); + } + } + + private void handleDele(String fileName) throws IOException { + if (!isAuthenticated) { + sendResponse(FTPResponse.NOT_LOGGED_IN, "Please login first"); + return; + } + + if (fileName.isEmpty()) { + sendResponse(FTPResponse.SYNTAX_ERROR_PARAMETERS, "No file name specified"); + return; + } + + if (fileSystem.deleteFile(fileName)) { + sendResponse(FTPResponse.REQUESTED_FILE_ACTION_OK, "File deleted"); + } else { + sendResponse(FTPResponse.FILE_UNAVAILABLE, "Failed to delete file"); + } + } + + private void handleSize(String fileName) throws IOException { + if (!isAuthenticated) { + sendResponse(FTPResponse.NOT_LOGGED_IN, "Please login first"); + return; + } + + if (fileName.isEmpty()) { + sendResponse(FTPResponse.SYNTAX_ERROR_PARAMETERS, "No file name specified"); + return; + } + + long size = fileSystem.getFileSize(fileName); + if (size >= 0) { + sendResponse(FTPResponse.FILE_STATUS, String.valueOf(size)); + } else { + sendResponse(FTPResponse.FILE_UNAVAILABLE, "File not found"); + } } private void handleNoop() throws IOException {