diff --git a/README.md b/README.md index 19ade23..bdd269f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Although it's named "minimal", it supports a bunch of features: * 100% Java, no libraries * Lightweight -* Supports 55 FTP commands +* Supports 57 FTP commands * TLS/SSL support * Custom File System support * Custom User Authentication support @@ -31,7 +31,7 @@ The required minimum implementation is already done, however, there are still co * [RFC 3659](https://tools.ietf.org/html/rfc3659) - Extensions to FTP (4/4) * [RFC 4217](https://tools.ietf.org/html/rfc4217) - Securing FTP with TLS * [RFC 5797](https://tools.ietf.org/html/rfc5797) - FTP Command and Extension Registry -* [RFC 7151](https://tools.ietf.org/html/rfc7151) - File Transfer Protocol HOST Command for Virtual Hosts (0/1) +* [RFC 7151](https://tools.ietf.org/html/rfc7151) - File Transfer Protocol HOST Command for Virtual Hosts (1/1) * [draft-twine-ftpmd5-00](https://tools.ietf.org/html/draft-twine-ftpmd5-00) - The "MD5" and "MMD5" FTP Command Extensions (2/2) (Obsolete) * [draft-somers-ftp-mfxx-04](https://tools.ietf.org/html/draft-somers-ftp-mfxx-04) - The "MFMT", "MFCT", and "MFF" Command Extensions for FTP (1/3) * [draft-bryan-ftpext-hash-02](https://tools.ietf.org/html/draft-bryan-ftpext-hash-02) - File Transfer Protocol HASH Command for Cryptographic Hashes (1/1) diff --git a/src/main/java/com/guichaguri/minimalftp/api/IUserAuthenticator.java b/src/main/java/com/guichaguri/minimalftp/api/IUserAuthenticator.java index 88ab19d..663df62 100644 --- a/src/main/java/com/guichaguri/minimalftp/api/IUserAuthenticator.java +++ b/src/main/java/com/guichaguri/minimalftp/api/IUserAuthenticator.java @@ -17,6 +17,7 @@ package com.guichaguri.minimalftp.api; import com.guichaguri.minimalftp.FTPConnection; +import java.net.InetAddress; /** * Represents an user authenticator. @@ -26,6 +27,19 @@ */ public interface IUserAuthenticator { + /** + * Whether the user is allowed to connect through the specified host. + * + * The client will not send the host address if {@link #needsUsername(FTPConnection)} returns {@code false} + * or the client does not support custom hosts. + * + * @param host The host address + * @return Whether the specified host is accepted + */ + default boolean acceptsHost(FTPConnection con, InetAddress host) { + return true; + } + /** * Whether this authenticator requires a username. * @@ -41,23 +55,27 @@ public interface IUserAuthenticator { * * @param con The FTP connection * @param username The username + * @param host The host address or {@code null} if it's not specified by the client * @return {@code true} if this authenticator requires a password */ - boolean needsPassword(FTPConnection con, String username); + boolean needsPassword(FTPConnection con, String username, InetAddress host); /** * Authenticates a user synchronously. * - * You use a custom file system depending on the user + * You can use a custom file system depending on the user. + * + * If the {@param host} is {@code null}, you can use a "default host" or a union of all hosts combined + * as some clients might not support custom hosts. * * @param con The FTP connection + * @param host The host address or {@code null} when the client didn't specified the hostname * @param username The username or {@code null} when {@link #needsUsername(FTPConnection)} returns false - * @param password The password or {@code null} when {@link #needsPassword(FTPConnection, String)} returns false + * @param password The password or {@code null} when {@link #needsPassword(FTPConnection, String, InetAddress)} returns false * @return A file system if the authentication succeeded * @throws AuthException When the authentication failed */ - IFileSystem authenticate(FTPConnection con, String username, String password) throws AuthException; - + IFileSystem authenticate(FTPConnection con, InetAddress host, String username, String password) throws AuthException; /** * The exception that should be thrown when the authentication fails diff --git a/src/main/java/com/guichaguri/minimalftp/handler/ConnectionHandler.java b/src/main/java/com/guichaguri/minimalftp/handler/ConnectionHandler.java index 745cbcd..e945871 100644 --- a/src/main/java/com/guichaguri/minimalftp/handler/ConnectionHandler.java +++ b/src/main/java/com/guichaguri/minimalftp/handler/ConnectionHandler.java @@ -20,10 +20,12 @@ import com.guichaguri.minimalftp.FTPServer; import com.guichaguri.minimalftp.Utils; import com.guichaguri.minimalftp.api.IUserAuthenticator; +import com.guichaguri.minimalftp.api.IUserAuthenticator.AuthException; import java.io.IOException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; +import java.net.UnknownHostException; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; @@ -36,6 +38,7 @@ public class ConnectionHandler { private final FTPConnection con; + private InetAddress address = null; private boolean authenticated = false; private String username = null; @@ -129,6 +132,8 @@ public void registerCommands() { con.registerCommand("EPSV", "EPSV", this::epsv); // Extended Passive Mode (RFC 2428) con.registerCommand("EPRT", "EPRT
", this::eprt); // Extended Active Mode (RFC 2428) + con.registerCommand("HOST", "HOST
", this::host, false); // Custom Virtual Hosts (RFC 7151) + con.registerFeature("base"); // Base Commands (RFC 5797) con.registerFeature("secu"); // Security Commands (RFC 5797) con.registerFeature("hist"); // Obsolete Commands (RFC 5797) @@ -139,6 +144,7 @@ public void registerCommands() { con.registerFeature("PROT"); // Protection Level (RFC 2228) con.registerFeature("EPSV"); // Extended Passive Mode (RFC 2428) con.registerFeature("EPRT"); // Extended Active Mode (RFC 2428) + con.registerFeature("HOST"); // Custom Virtual Hosts (RFC 7151) } private void noop() { @@ -191,6 +197,28 @@ private void mode(String mode) throws IOException { } } + private void host(String host) throws IOException { + if(authenticated) { + con.sendResponse(503, "The user is already authenticated"); + return; + } + + try { + IUserAuthenticator auth = con.getServer().getAuthenticator(); + InetAddress address = InetAddress.getByName(host); + + if(auth.acceptsHost(con, address)) { + this.address = address; + con.sendResponse(220, "Host accepted"); + } else { + this.address = null; + con.sendResponse(504, "Host denied"); + } + } catch(UnknownHostException ex) { + con.sendResponse(501, "Invalid host"); + } + } + private void user(String username) throws IOException { if(authenticated) { con.sendResponse(230, "Logged in!"); @@ -200,7 +228,7 @@ private void user(String username) throws IOException { this.username = username; IUserAuthenticator auth = con.getServer().getAuthenticator(); - if(auth.needsPassword(con, username)) { + if(auth.needsPassword(con, username, address)) { // Requests a password for the authentication con.sendResponse(331, "Needs a password"); } else { @@ -257,6 +285,7 @@ private void syst() { private void rein() { authenticated = false; username = null; + address = null; con.sendResponse(220, "Ready for a new user"); } @@ -444,10 +473,13 @@ private void eprt(String data) { private boolean authenticate(IUserAuthenticator auth, String password) { try { - con.setFileSystem(auth.authenticate(con, username, password)); + con.setFileSystem(auth.authenticate(con, address, username, password)); authenticated = true; return true; + } catch(AuthException ex) { + return false; } catch(Exception ex) { + ex.printStackTrace(); return false; } } diff --git a/src/main/java/com/guichaguri/minimalftp/impl/NoOpAuthenticator.java b/src/main/java/com/guichaguri/minimalftp/impl/NoOpAuthenticator.java index 9ddea23..2325257 100644 --- a/src/main/java/com/guichaguri/minimalftp/impl/NoOpAuthenticator.java +++ b/src/main/java/com/guichaguri/minimalftp/impl/NoOpAuthenticator.java @@ -19,6 +19,7 @@ import com.guichaguri.minimalftp.FTPConnection; import com.guichaguri.minimalftp.api.IFileSystem; import com.guichaguri.minimalftp.api.IUserAuthenticator; +import java.net.InetAddress; /** * No Operation Authenticator @@ -45,12 +46,12 @@ public boolean needsUsername(FTPConnection con) { } @Override - public boolean needsPassword(FTPConnection con, String username) { + public boolean needsPassword(FTPConnection con, String username, InetAddress address) { return false; } @Override - public IFileSystem authenticate(FTPConnection con, String username, String password) throws AuthException { + public IFileSystem authenticate(FTPConnection con, InetAddress address, String username, String password) throws AuthException { return fs; } } diff --git a/src/test/java/com/guichaguri/minimalftp/custom/UserbaseAuthenticator.java b/src/test/java/com/guichaguri/minimalftp/custom/UserbaseAuthenticator.java index 87fff9f..1c86944 100644 --- a/src/test/java/com/guichaguri/minimalftp/custom/UserbaseAuthenticator.java +++ b/src/test/java/com/guichaguri/minimalftp/custom/UserbaseAuthenticator.java @@ -5,6 +5,7 @@ import com.guichaguri.minimalftp.api.IUserAuthenticator; import com.guichaguri.minimalftp.impl.NativeFileSystem; import java.io.File; +import java.net.InetAddress; import java.security.MessageDigest; import java.util.Arrays; import java.util.HashMap; @@ -37,12 +38,12 @@ public boolean needsUsername(FTPConnection con) { } @Override - public boolean needsPassword(FTPConnection con, String username) { + public boolean needsPassword(FTPConnection con, String username, InetAddress address) { return true; } @Override - public IFileSystem authenticate(FTPConnection con, String username, String password) throws AuthException { + public IFileSystem authenticate(FTPConnection con, InetAddress address, String username, String password) throws AuthException { // Check for a user with that username in the database if(!userbase.containsKey(username)) { throw new AuthException();