package org.dcache.ftp.door; import org.dcache.dss.DssContext; import org.dcache.dss.DssContextFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.security.auth.Subject; import java.io.IOException; import java.nio.charset.Charset; import java.util.Base64; import diskCacheV111.util.CacheException; import diskCacheV111.util.PermissionDeniedCacheException; import dmg.util.CommandExitException; import org.dcache.auth.LoginNamePrincipal; public abstract class GssFtpDoorV1 extends AbstractFtpDoorV1 { private static final Logger LOGGER = LoggerFactory.getLogger(GssFtpDoorV1.class); public static final String GLOBUS_URL_COPY_DEFAULT_USER = ":globus-mapping:"; private static final Charset UTF8 = Charset.forName("UTF-8"); protected Subject subject; // GSS general protected String gssFlavor; protected DssContext context; private DssContextFactory dssContextFactory; public GssFtpDoorV1(String ftpDoorName, String tlogName, String gssFlavor, DssContextFactory dssContextFactory) { super(ftpDoorName, tlogName); this.gssFlavor = gssFlavor; this.dssContextFactory = dssContextFactory; } @Override protected void secure_reply(String answer, String code) { answer = answer + "\r\n"; byte[] data = answer.getBytes(UTF8); try { data = context.wrap(data, 0, data.length); } catch (IOException e) { reply("500 Reply encryption error: " + e); return; } println(code + " " + Base64.getEncoder().encodeToString(data)); } @Help("AUTH <SP> <arg> - Initiate secure context negotiation.") public void ftp_auth(String arg) throws FTPCommandException { LOGGER.info("GssFtpDoorV1::secure_reply: going to authorize using {}", gssFlavor); if (!arg.equals("GSSAPI")) { /* From RFC 2228 Section 3. New FTP Commands, AUTH: * * If the server does not understand the named security * mechanism, it should respond with reply code 504. */ throw new FTPCommandException(504, "Authenticating method not supported"); } if (context != null && context.isEstablished()) { /* From RFC 2228 Section 3. New FTP Commands, AUTH: * * Some servers will allow the AUTH command to be reissued in * order to establish new authentication. [...] * * That dCache does not allow re-authentication is allowed by the * RFC, but the RFC does not mention which return code is to be used * when rejecting the command. "534 Request denied for policy * reasons" seems the best fit. */ throw new FTPCommandException(534, "Already authenticated"); } try { context = dssContextFactory.create(_remoteSocketAddress, _localSocketAddress); } catch (IOException e) { LOGGER.error("Unable to initialise service context: {}", e.toString()); /* From RFC 2228 Section 3. New FTP Commands, AUTH: * * If the server is not able to accept the named security * mechanism, such as if a required resource is unavailable, it * should respond with reply code 431. */ throw new FTPCommandException(431, "Internal error"); } reply("334 ADAT must follow"); } @Help("ADAT <SP> <arg> - Supply context negotation data.") public void ftp_adat(String arg) { if (arg == null || arg.length() <= 0) { reply("501 ADAT must have data"); return; } if (context == null) { reply("503 Send AUTH first"); return; } byte[] token = Base64.getDecoder().decode(arg); try { //_serviceContext.setChannelBinding(cb); //debug("GssFtpDoorV1::ftp_adat: CB set"); token = context.accept(token); //debug("GssFtpDoorV1::ftp_adat: Token created"); subject = context.getSubject(); //debug("GssFtpDoorV1::ftp_adat: User principal: " + UserPrincipal); } catch (IOException e) { LOGGER.trace("Authentication failed", e); reply("535 Authentication failed: " + e.getMessage()); return; } if (token != null) { if (!context.isEstablished()) { reply("335 ADAT=" + Base64.getEncoder().encodeToString(token)); } else { reply("235 ADAT=" + Base64.getEncoder().encodeToString(token)); } } else { if (!context.isEstablished()) { reply("335 ADAT="); } else { LOGGER.info("GssFtpDoorV1::ftp_adat: security context established with {}", subject); reply("235 OK"); } } } @Help("CCC - Switch control channel to cleartext.") public void ftp_ccc(String arg) { // We should never received this, only through MIC, ENC or CONF, // in which case it will be intercepted by secure_command() reply("533 CCC must be protected"); } @Help("MIC <SP> <arg> - Integrity protected command.") public void ftp_mic(String arg) throws CommandExitException { secure_command(arg, "mic"); } @Help("ENC <SP> <arg> - Privacy protected command.") public void ftp_enc(String arg) throws CommandExitException { secure_command(arg, "enc"); } @Help("CONF <SP> <arg> - Confidentiality protection command.") public void ftp_conf(String arg) throws CommandExitException { secure_command(arg, "conf"); } public void secure_command(String answer, String sectype) throws CommandExitException { if (answer == null || answer.length() <= 0) { reply("500 Wrong syntax of " + sectype + " command"); return; } if (context == null || !context.isEstablished()) { reply("503 Security context is not established"); return; } byte[] data = Base64.getDecoder().decode(answer); try { data = context.unwrap(data); } catch (IOException e) { reply("500 Can not decrypt command: " + e); LOGGER.error("GssFtpDoorV1::secure_command: got IOException: {}", e.getMessage()); return; } // At least one C-based client sends a zero byte at the end // of a secured command. Truncate trailing zeros. // Search from the right end of the string for a non-null character. int i; for (i = data.length; i > 0 && data[i - 1] == 0; i--) { //do nothing, just decrement i } String msg = new String(data, 0, i, UTF8); msg = msg.trim(); if (msg.toLowerCase().startsWith("pass") && msg.length() != 4) { _commandLine = sectype.toUpperCase() + "{" + msg.substring(0, 4) + " ...}"; } else { _commandLine = sectype.toUpperCase() + "{" + msg + "}"; } if (msg.equalsIgnoreCase("CCC")) { _gReplyType = "clear"; reply("200 OK"); } else { _gReplyType = sectype; ftpcommand(msg); } } @Override public void ftp_user(String arg) { if (arg.equals("")) { reply(err("USER", arg)); return; } if (context == null || !context.isEstablished()) { reply("530 Authentication required"); return; } Subject subject = context.getSubject(); subject.getPrincipals().add(_origin); if (!arg.equals(GLOBUS_URL_COPY_DEFAULT_USER)) { subject.getPrincipals().add(new LoginNamePrincipal(arg)); } try { login(subject); reply("200 User " + arg + " logged in", this.subject); } catch (PermissionDeniedCacheException e) { LOGGER.warn("Login denied for {}: {}", context.getPeerName(), e.getMessage()); println("530 Login denied"); } catch (CacheException e) { LOGGER.error("Login failed for {}: {}", context.getPeerName(), e.getMessage()); println("530 Login failed: " + e.getMessage()); } } // Some clients, even though the user is already logged in via GSS and ADAT, // will send a dummy PASS anyway. "Already logged in" is distracting // and the "Going to evaluate strong password" message is misleading // since nothing is actually done for this command. // Example = ubftp client @Override public void ftp_pass(String arg) { LOGGER.debug("GssFtpDoorV1::ftp_pass: PASS is a no-op with " + "GSSAPI authentication."); if (subject != null) { reply(ok("PASS")); } else { reply("500 Send USER first"); } } }