All Downloads are FREE. Search and download functionalities are using the official Maven repository.

lrgs.ldds.CmdAuthHello Maven / Gradle / Ivy

Go to download

A collection of software for aggregatting and processing environmental data such as from NOAA GOES satellites.

The newest version!
/*
*  $Id: CmdAuthHello.java,v 1.10 2020/03/10 16:30:58 mmaloney Exp $
*/
package lrgs.ldds;

import java.io.File;
import java.io.IOException;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.Date;
import java.util.NoSuchElementException;
import java.text.SimpleDateFormat;
import java.text.ParseException;

import ilex.util.Logger;
import ilex.util.EnvExpander;
import ilex.util.PasswordFile;
import ilex.util.PasswordFileEntry;
import ilex.util.TextUtil;
import lrgs.common.*;
import lrgs.apiadmin.AuthenticatorString;
import lrgs.db.DbPasswordFile;
import lrgs.db.DdsConnectionStats;
import lrgs.db.LrgsDatabase;
import lrgs.db.LrgsDatabaseThread;
import lrgs.ddsserver.DdsServer;
import lrgs.lrgsmain.LrgsConfig;

/**
This class handles the authenticated hello message on the server side.
*/
public class CmdAuthHello extends LddsCommand
{
	/** The user name */
	String username;

	/** time string supplied by client */
	String timestr;

	/** authenticator supplied by client */
	String authenticator;

	/** Unchangable password file name on the server machine. */
	public static final String LRGS_PASSWORD_FILE_NAME = 
		"$LRGSHOME/.lrgs.passwd";

	/** Unchangable local password file name on the server machine. */
	public static final String LRGS_LOCAL_PASSWORD_FILE_NAME = 
		"$LRGSHOME/.lrgs.passwd.local";

	/** Alternate directory for the password file. */
	public static final String ALT_LRGS_PASSWORD_FILE_NAME = "~/.lrgs.passwd";

	/** Maximum allowable clock difference: 20 min. */
	public static final long MAX_CLOCK_DIFF = (20L * 60L * 1000L);

	private String ddsVersion = null;

	private SimpleDateFormat goesDateFormat;

	/**
	  Create a new 'AuthHello' request with the specified user name.
	  @param data body of the request from client.
	*/
	public CmdAuthHello(byte data[])
	{
		String args = new String(data, 0, 
			(data[data.length-1] == (byte)0) ? data.length-1 : data.length);

		goesDateFormat = new SimpleDateFormat("yyDDDHHmmss");
		TimeZone jtz = TimeZone.getTimeZone("UTC");
		goesDateFormat.setTimeZone(jtz);

		StringTokenizer st = new StringTokenizer(args);

		username = null;
		timestr = null;
		authenticator = null;

		try
		{
			username = st.nextToken();
			timestr = st.nextToken();
			authenticator = st.nextToken();
			if (st.hasMoreTokens())
				ddsVersion = st.nextToken();

			Logger.instance().debug1(DdsServer.module+" Received "
				+ toString()
				+ (ddsVersion==null ? "" : (" dds="+ddsVersion)));
		}
		catch(NoSuchElementException e)
		{
			Logger.instance().warning(
				"Invalid arguments in AuthHello message '" + args + "'");
		}
	}

	/** @return "CmdAuthHello"; */
	public String cmdType()
	{
		return "CmdAuthHello";
	}

	/**
	  Attach to LRGS as the specified user.
	  Throw IllegalArgumentException if the username is unknown or if
	  user is already connected and multiple connections are disallowed.
	  @param ldds the server thread object
	  @throws UnknownUserException if the user is not known.
	  @throws AuthFailedException if authentication or authorization fails.
	*/
	public int execute(LddsThread ldds)
		throws ArchiveException, IOException
	{
		Logger.instance().debug1(DdsServer.module +
			" AuthHello execute for client " + ldds.getClientName());

		if (username == null || timestr == null || authenticator == null)
		{
			Logger.instance().warning(DdsServer.module +
				" Received AuthHello missing required args from client " 
				+ ldds.getClientName());
			throw new UnknownUserException(
				"AuthHello missing required arguments", true);
		}

		long mt = 0;
		try
		{
			Date d = goesDateFormat.parse(timestr);
			mt = d.getTime();
		}
		catch(ParseException pe)
		{
			Logger.instance().warning(DdsServer.module +
				" Received AuthHello with invalid time format from user "
				+ username + " at " + ldds.getHostName());
			throw new LddsRequestException(toString()
				+ ", invalid time format '" + timestr 
				+ "', must be YYDDDHHMMSS", 
				LrgsErrorCode.DDDSAUTHFAILED, true);
		}

		// Verify that time is within 20 minutes.
		long diff = System.currentTimeMillis() - mt;
		if (diff < 0) 
			diff = -diff;
		if (diff > MAX_CLOCK_DIFF)
		{
			Logger.instance().warning(DdsServer.module +
				" Received AuthHello with clockdiff = " + (diff/1000L)
				+ " seconds from client " + username + "@" + ldds.getHostName());
			throw new LddsRequestException(toString()
				+ ", time stamp difference too large ("
				+ (diff/1000L) + " seconds)",
				LrgsErrorCode.DDDSAUTHFAILED, true);
		}

		PasswordFileEntry pfe;
		try { pfe = getPasswordFileEntry(username); }
		catch(UnknownUserException ex)
		{
			Logger.instance().warning(DdsServer.module +
				" Received AuthHello with bad username '" + username
				+ "' from host " + ldds.getHostName());
			ldds.myStats.setSuccessCode(ldds.myStats.SC_BAD_USERNAME);
			ldds.statLogger.incrBadUsernames();
			ex.setHangup(true);
			throw ex;
		}

		// Make sure this user has the 'dds' role defined.
		if (!pfe.isRoleAssigned("dds"))
		{
			ldds.myStats.setSuccessCode(DdsConnectionStats.SC_NO_DDS_PERM);
			String msg = "Rejecting connection from user '" + username 
				+ "' -- does not have permission to use DDS";
			Logger.instance().warning(DdsServer.module + " " + msg);
			throw new AuthFailedException(msg);
		}
		
		// LddsUser ctor will throw UnknownUserException if no sandbox dir.
		boolean isLocal = TextUtil.str2boolean(pfe.getProperty("local"));
		LrgsConfig cfg = LrgsConfig.instance();
		String userRoot = isLocal ? cfg.ddsUserRootDirLocal : cfg.ddsUserRootDir;
		LddsUser user = new LddsUser(username, userRoot);
		if (ddsVersion != null)
			user.setClientDdsVersion(ddsVersion);

		// Construct an authenticator & compare to the one passed.
		AuthenticatorString authstr = null;
		int timet;
		String constructed = "";
		String algo = AuthenticatorString.ALGO_SHA256;
		try
		{
			timet = (int)(mt/1000);
			authstr = new AuthenticatorString(timet, pfe, algo);
			constructed = authstr.getString();
			if (!constructed.equals(authenticator))
			{
				// No match with SHA-256, Try again with SHA.
				authstr = new AuthenticatorString(timet, pfe, algo = AuthenticatorString.ALGO_SHA);
				constructed = authstr.getString();
			}
		}
		catch (java.security.NoSuchAlgorithmException nsae)
		{
			throw new LddsRequestException(algo + " not supported in JVE",
				LrgsErrorCode.DDDSINTERNAL, true);
		}

		if (!constructed.equals(authenticator))
		{
			// Didn't match either SHA256 or SHA.
			ldds.myStats.setSuccessCode(DdsConnectionStats.SC_BAD_PASSWORD);
			ldds.myStats.setUserName(username);
			ldds.statLogger.incrBadPasswords();
			String msg = DdsServer.module + 
				" Rejecting connection from user '" + username 
				+ "' -- bad password used from client " + ldds.getClientName();
			Logger.instance().warning(msg);
			
			// If this is the 3rd consecutive bad password, suspend account for 30 sec.
			if (LrgsConfig.instance().getPasswordChecker() != null)
			{
				LrgsDatabaseThread ldt = LrgsDatabaseThread.instance();
				if (ldt != null)
				{
					LrgsDatabase lrgsDb = ldt.getLrgsDb();
					if (lrgsDb != null)
					{
						if (lrgsDb.getNumConsecutiveBadPasswords(username) == 4)
						{
							Logger.instance().warning(
								"User '" + username + "' -- 5 consecutive bad passwords. "
								+ "Account will be suspended for 5 minutes.");
							user.suspendUntil(new Date(System.currentTimeMillis() + 5*60000L));
						}
					}
				}
			}
			throw new LddsRequestException("Bad password", LrgsErrorCode.DDDSAUTHFAILED, true);
		}
		
		// If I get to here, it means the constructed authenticator matched what the user sent.
		if (algo == AuthenticatorString.ALGO_SHA && LrgsConfig.instance().reqStrongEncryption)
		{
			// The match was with SHA, but this server requires SHA256.
			if (ldds.secondAuthAttempt)
			{
				ldds.myStats.setSuccessCode(DdsConnectionStats.SC_BAD_PASSWORD);
				ldds.myStats.setUserName(username);
				ldds.statLogger.incrBadPasswords();
				String msg = DdsServer.module + 
					" Rejecting connection from user '" + username 
					+ "' -- bad password used from client " + ldds.getClientName();
				Logger.instance().warning(msg);
				throw new LddsRequestException("Server requires SHA-256.", LrgsErrorCode.DDDSAUTHFAILED, true);
			}
			else // Allow them to try again with SHA-256
			{
				String msg = DdsServer.module + 
					" Received SHA auth from user '" + username 
					+ "' -- but this server requires SHA-256. " + ldds.getClientName();
				Logger.instance().debug1(msg);
				ldds.secondAuthAttempt = true;
				throw new LddsRequestException("Server requires SHA-256.", LrgsErrorCode.DSTRONGREQUIRED, false);
			}
		}

		// If I get to here, the user is authenticated, wither with SHA-256 or SHA.
		if (algo == AuthenticatorString.ALGO_SHA256)
		{
			try
			{
				if (ddsVersion != null)
				{
					int dv = Integer.parseInt(ddsVersion);
					// TODO: Should we check for version number >= 13, or is there
					// any harm in letting earlier versions go as long as they support
					// proper authentication?
				}
			}
			catch(NumberFormatException ex)
			{
				throw new LddsRequestException("Protocol Error",
					LrgsErrorCode.DDDSAUTHFAILED, true);
			}
		}
		
		// Some users are restricted by IP Address
		checkValidIpAddress(pfe, ldds, username);

		user.isAuthenticated = true;
		user.setLocal(isLocal);
		try
		{
			// Session key will use the same algorithm as selected above.
			user.setSessionKey(AuthenticatorString.makeAuthenticator(
				authstr.getString().getBytes(), pfe.getShaPassword(), timet, algo));
			user.isAdmin = pfe.isRoleAssigned("admin");
			if (!isLocal && cfg.localAdminOnly)
				user.isAdmin = false;
		}
		catch (java.security.NoSuchAlgorithmException nsae)
		{
			// Can't happen if we got this far!
		}
		
		String x = pfe.getProperty("disableBackLinkSearch");
		if (x == null)
			x = pfe.getProperty("forceAscending");
		if (x != null)
			user.setDisableBackLinkSearch(TextUtil.str2boolean(x));
		x = pfe.getProperty("goodOnly");
//Logger.instance().info("User '" + ldds.getClientName() + "' goodOnly=" + x);
		if (x != null)
			user.setGoodOnly(TextUtil.str2boolean(x));

		// Callback to thread to attach to LRGS as this user.
		ldds.attachLrgs(user);

		getDcpLimit(pfe, ldds);
		
		// OpenDCS 6.2 NOAA enhancements for suspending an account.
		if (user.isSuspended())
		{
			ldds.myStats.setSuccessCode(DdsConnectionStats.SC_ACCOUNT_SUSPENDED);
			throw new LddsRequestException("Account suspended.", LrgsErrorCode.DDDSAUTHFAILED, true);
		}


		// Echo AuthHELLO with username and proto version as an acknowledgement.
		LddsMessage msg = new LddsMessage(LddsMessage.IdAuthHello, 
			username + " " + timestr + " " + DdsVersion.DdsVersionNum);
		ldds.send(msg);

		if (user.isAdmin)
			Logger.instance().info(DdsServer.module + " ADMIN authenticated connection from "
				+ ldds.getClientName() + " (algo=" + algo + ")");
		else
			Logger.instance().debug1(DdsServer.module + " " + ldds.getClientName() 
				+ " - Successfully authenticated (algo=" + algo + ")!");
		return 0;
	}

	public static void main(String args[])
		throws Exception
	{
		PasswordFileEntry pfe = getPasswordFileEntry(args[0]);
		if (pfe == null)
			System.out.println("No such user");
		else
			System.out.println(pfe.toString());
	}
	/**
	 * Read the password file and extract the entry for the named user.
	 * @param user the user to search for.
	 * @return the PasswordFileEntry for the named user
	 * @throws UnknownUserException if the user cannot be found.
	 */
	public static synchronized PasswordFileEntry getPasswordFileEntry(String user)
		throws UnknownUserException
	{
		LrgsDatabase lrgsDb = LrgsDatabaseThread.instance().getLrgsDb();
		if (lrgsDb != null)
		{
			DbPasswordFile dpf = new DbPasswordFile(null, lrgsDb);
			PasswordFileEntry pfe = dpf.readSingle(user);
			if (pfe != null)
				return pfe;
			// else fall through and try the flat files...
		}
		
		// Read the password file entry for this user.
		String fileNames[] = { LRGS_LOCAL_PASSWORD_FILE_NAME,
			LRGS_PASSWORD_FILE_NAME, ALT_LRGS_PASSWORD_FILE_NAME };
		PasswordFile pf;
		for(String fn : fileNames)
		{
			String pfname = EnvExpander.expand(fn);
			try
			{
				pf = new PasswordFile(new File(pfname));
				pf.read();
				Logger.instance().debug1(DdsServer.module 
					+ " Read main password file at " + pfname);
				PasswordFileEntry pfe = pf.getEntryByName(user);
				if (pfe != null)
				{
					if (fn.equals(LRGS_LOCAL_PASSWORD_FILE_NAME))
					{
						pfe.setProperty("local", "true");
						pfe.setLocal(true);
					}
					return pfe;
				}
				Logger.instance().debug1(DdsServer.module
					+ " No such user '" + user + "' in file '" + pfname
					+ "'");
			}
			catch(IOException ioe)
			{
				Logger.instance().debug1("Cannot read password file '" 
					+ pfname + "'");
			}
		}
		throw new UnknownUserException("No such user '" + user + "'");
	}

	/**
	 * Checks to see if the user is connecting from a valid IP address if a
	 * restriction is defined for this user.
	 * @throws AuthFailedException if invalid
	 */
	public static void checkValidIpAddress(PasswordFileEntry pfe, 
		LddsThread ldds, String username)
		throws AuthFailedException
	{
		if (pfe == null)
			return; // No restrictions if no entry in password file.

		// Check to see if user is connecting from a valid IP address.
		String addrList = pfe.getProperty("ipaddr");
		if (addrList == null)
			return;

		String sockIpAddr = ldds.getSocket().getInetAddress().getHostAddress();

		StringTokenizer st = new StringTokenizer(addrList, ";");
		boolean matchFound = false;
		while(st.hasMoreTokens())
		{
			String allowedIpAddr = st.nextToken();
			int idx = allowedIpAddr.indexOf('*');
			if (idx != -1)
				allowedIpAddr = allowedIpAddr.substring(0, idx);
			if (sockIpAddr.startsWith(allowedIpAddr))
			{
				matchFound = true;
				break;
			}
		}
		if (!matchFound)
		{
			ldds.myStats.setSuccessCode(DdsConnectionStats.SC_BAD_IP_ADDR);
			String msg = DdsServer.module + " User '" + username +
				"' Attempt to connect from disallowed IP Address ("
				+ sockIpAddr + ")";
			Logger.instance().warning(msg);
			throw new AuthFailedException(msg);
		}
	}

	public static void getDcpLimit(PasswordFileEntry pfe, LddsThread ldds)
	{
		String dcpLimitStr;
		if (pfe != null
		 && (dcpLimitStr = pfe.getProperty("maxdcps")) != null)
		{
			try { ldds.user.dcpLimit = Integer.parseInt(dcpLimitStr); }
			catch(NumberFormatException ex)
			{
				Logger.instance().warning("User '" + ldds.getUserName()
					+ "' has invalid maxdcps property in the password file."
					+ " -- no limit will be imposed.");
				ldds.user.dcpLimit = -1;
			}
		}
		else
			ldds.user.dcpLimit = -1;
	}

	/** @return a string representation for log messages. */
	public String toString()
	{
		return "AuthHello from '" + username + "'";
	}

	/** @return the code associated with this command. */
	public char getCommandCode() { return LddsMessage.IdAuthHello; }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy