/* Part of the Das2 libraries, which the LGPL with class-path exception license */ package org.das2.util; import java.awt.Frame; import java.awt.Window; import java.io.Console; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.ImageIcon; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; /** Provides per-resource login credentials * * This class maintains a map of login credentials by resource ID. The resource ID's * themselves are just strings. The only expectation on resource ID strings is that they * should be suitable for use as the Keys to a hash map. Otherwise no formation rules are * assumed nor expected. User names and passwords for multiple web-sites, ftp sites, etc. * are maintained by a single instance of this class. Call: * * CredentialsManager.getManager() * * to get a reference to that single instance. * * In a graphical environment this class handles presenting dialogs to the user to gather * logon credentials. In a shell environment it will interact with the TTY to get user * information. * * An example of using this class to handle Das 2.1 server authentication which html * location information formatting follows: * * * CredentialsManage cm = CrentialsManager.getMannager(); * String sLocId = "planet.physics.uiowa.edu/das/das2Server|voyager1/pwi/SpecAnalyzer-4s-Efield"; * * if(!cm.hasCredentials(sLocId)){ * DasServer svr = DasServer.create(sDsdf); * String sDesc = String.Format("

%s

Server: %s

DataSource: %s

", * DasServer.getName(), "planet.physics.uiowa.edu", * "voyager1 > pwi > SpecAnalyzer-4s-Efield"); * cm.setDescription(sLocId, sDesc, DasServer.getLogo()); * } * * String sHash = getHttpBasicHash(sLocId) * *
* * Two previous classes, org.das2.util.filesystem.KeyChain (autoplot) and * org.das2.client.Authenticator have approached this problem as well. However both of * those classes make assumptions that are not valid in general. The first assumes that * the caller somehow knows the username. The second assumes that you are talking to * a first generation Das 2.1 server. Details of server communication are beyond the * scope of this class. * * @author cwp */ public class CredentialsManager{ private static final Logger logger= LoggerManager.getLogger("das2.credentialsmanager"); /////////////////////////////////////////////////////////////////////////////////// // Static Section // A map of credentials managers versus lookup key static final HashMap g_dManagers = new HashMap(); static{ g_dManagers.put(null, new CredentialsManager(null)); } /** Get a reference to the authentication manager. * * Typically this is the function you want to use to get started. */ public static CredentialsManager getMannager(){ return g_dManagers.get(null); } /** Get an authentication manager associated with an associated tracking string. * This is probably not the function you are looking for. It only exists for * odd cases where two different credential managers are active in a single * application at the same time. * * @param sWhich - A string used to differentiate this credentials manager from the * default instance. */ public static CredentialsManager getMannager(String sWhich){ if(!g_dManagers.containsKey(sWhich)){ synchronized(g_dManagers) { g_dManagers.put(sWhich, new CredentialsManager(sWhich)); } } return g_dManagers.get(sWhich); } //////////////////////////////////////////////////////////////////////////////////// // Instance Section protected static class Location{ String sLocId; String sDesc; ImageIcon iconLogo; String sUser; String sPasswd; protected Location(String sLocationId, String sDescription, ImageIcon icon){ sLocId = sLocationId; sDesc = sDescription; iconLogo = icon; sUser = null; sPasswd = null; } protected boolean hasCredentials(){ return (sUser != null)||(sPasswd != null); } } String m_sName; HashMap m_dLocs; CredentialsDialog m_dlg; protected CredentialsManager(String sName){ m_sName = sName; m_dLocs = new HashMap(); m_dlg = null; } /** Provide a description of a location for use in authentication dialogs. * * Use this function to tie a string description to a location id. This description * will be used when interacting with the user. If no description is present, then just * the location ID itself will be used to identify the site to the end user. * Usually location strings aren't that easy to read so use of this function or the * version with an icon argument is recommended, though not required. * * @param sLocationId The location to describe, can not be null. * @param sDescription A string to present to a user when prompting for a credentials * for this location, may be a basic HTML string. */ public void setDescription(String sLocationId, String sDescription){ setDescription(sLocationId, sDescription, null); } /** Provide a description of a location with an Image Icon * * Use this function to tie a string description and an Icon to a location. * * @param sLocationId The location to describe, can not be null. * @param sDescription The description, may be a simply formatted HTML string. * @param icon An icon to display for the server. */ public synchronized void setDescription(String sLocationId, String sDescription, ImageIcon icon) { if(!m_dLocs.containsKey(sLocationId)){ m_dLocs.put(sLocationId, new Location(sLocationId, sDescription, icon)); } else{ Location loc = m_dLocs.get(sLocationId); loc.sDesc = sDescription; loc.iconLogo = icon; } } /** Determine if there are any stored credentials for this location * * If either a username or a password have been provided for the location * then it is considered to have credentials * * @param sLocationId The location to describe, can not be null. * @return true if there are stored credentials, false otherwise */ public boolean hasCredentials(String sLocationId){ if(!m_dLocs.containsKey(sLocationId)) return false; Location loc = m_dLocs.get(sLocationId); return loc.hasCredentials(); } /** * Allow scripts to set username and password. * @param sLocationId the location/realm, like "http://jupiter.physics.uiowa.edu/das/server|Juno Magnetospheric Working Group" * @param userInfo the user pass in string like "usern:passw" */ public void setHttpBasicHashRaw(String sLocationId, String userInfo){ if(!hasCredentials(sLocationId)){ m_dLocs.put(sLocationId, new Location(sLocationId, null, null)); } Location loc = m_dLocs.get(sLocationId); String[] ss = userInfo.split(":", -2); //TODO: allow colons in passwords loc.sUser = ss[0]; loc.sPasswd = ss[1]; } /** Determine if a given location has been described * * Gathering descriptive information about a remote location may trigger communication * with a remote site. Use this function to see if such communication is needed. * * @param sLocationId The location in question * @return true if the location has been described, false otherwise */ public boolean hasDescription(String sLocationId){ if(!m_dLocs.containsKey(sLocationId)) return false; Location loc = m_dLocs.get(sLocationId); return (loc.sDesc != null)&&(!loc.sDesc.isEmpty()); } /** Determine is a site image has been set for a location ID. * * This function is provided because retrieving the logo for a site may trigger remote * host communication. Use this function to see if such communication is needed. * @param sLocationId the location in question * @return true if the location has as attached icon logo */ public boolean hasIcon(String sLocationId){ if(!m_dLocs.containsKey(sLocationId)) return false; Location loc = m_dLocs.get(sLocationId); return loc.iconLogo != null; } /** Get credentials in the form of a hashed HTTP Basic authentication string * * If there are no credentials stored for the given location id, this function may * trigger interaction with the user, such as presenting modal dialogs, or changing the * TTY to non-echo. * * @param sLocationId A unique string identifying a location. There are no formation * rules on the string, but convenience functions are provided if a uniform naming * convention is desired. * * @return The string USERNAME + ":" + PASSWORD that is then run through a base64 * encoding. If no credentials are available for the given location ID and none can be * gathered from the user (possibly due to the java.awt.headless being set or the * user pressing cancel), null is returned. * @see #getHttpBasicHashRaw(java.lang.String) */ public String getHttpBasicHash(String sLocationId){ String sTmp= getHttpBasicHashRaw( sLocationId ); if ( sTmp==null ) { return null; } else { String sHash = Base64.getEncoder().encodeToString(sTmp.getBytes()); return sHash; } } /** Get credentials in the form of a hashed HTTP Basic authentication string. * * If there are no credentials stored for the given location id, this function * may trigger interaction with the user, such as presenting modal dialogs, or * changing the TTY to non-echo. * * @param sLocationId A unique string identifying a location. There are no formation * rules on the string, but convenience functions are provided if a uniform naming * convention is desired. * * @return The string USERNAME + ":" + PASSWORD. If no credentials are available for * the given location ID and none can be gathered from the user (possibly due to the * java.awt.headless being set or the user pressing cancel), null is returned. * @see #getHttpBasicHash(java.lang.String) */ public String getHttpBasicHashRaw(String sLocationId){ if(!m_dLocs.containsKey(sLocationId)){ synchronized(this){ //Check again. Though unlikely, the key could have been added between //the call above and the start of the sychronized section if(!m_dLocs.containsKey(sLocationId)){ m_dLocs.put(sLocationId, new Location(sLocationId, null, null)); } } } Location loc = m_dLocs.get(sLocationId); if(!hasCredentials(sLocationId)){ if("true".equals( System.getProperty("java.awt.headless"))){ if(!getCredentialsCmdLine(loc)) return null; } else{ if(!getCredentialsGUI(loc)) return null; } } String sTmp = loc.sUser + ":" + loc.sPasswd; return sTmp; } /** Let the credentials manager know that stored credentials for a location are invalid * * @param sLocationId * @return */ public synchronized void invalidate(String sLocationId){ if(!m_dLocs.containsKey(sLocationId)) return; Location loc = m_dLocs.get(sLocationId); loc.sUser = null; loc.sPasswd = null; } /** * support restricted security environment by checking permissions before * checking property. * @param name * @param deft * @return * @see org.das2.DasApplication#getProperty ! */ public static String getProperty( String name, String deft ) { try { return System.getProperty(name, deft); } catch ( SecurityException ex ) { return deft; } } /** * returns the location of the local directory sandbox. For example, * The web filesystem object downloads temporary files to here, logging * properties file, etc. * * Assume that this File is local, so I/O is quick, and that the process * has write access to this area. * For definition, assume that at least 1Gb of storage is available as * well. * * @return the directory. * @see org.das2.DasApplication#getDas2UserDirectory ! */ public static File getDas2UserDirectory() { File local; // for applets, if we are running from a disk, then it is okay to write local files, but we can't check permissions if ( getProperty("user.name", "Web").equals("Web") ) { local= new File("/tmp"); } else { local= new File( System.getProperty("user.home") ); } local= new File( local, ".das2" ); return local; } ////////////////////////// User Interaction //////////////////////////////////// private String checkKeyChainForCredentials( Location loc ) { File das2Dir= getDas2UserDirectory(); if ( !das2Dir.exists() ) { if ( !das2Dir.mkdirs() ) { logger.log(Level.WARNING, "unable to mkdir {0}", das2Dir); return null; } } File credentialsDir= new File( das2Dir, "keychain" ); if ( !credentialsDir.exists() ) { if ( !credentialsDir.mkdirs() ) { logger.log(Level.WARNING, "unable to mkdir {0}", credentialsDir); return null; } } String hash= String.format( "%09d.txt", loc.sLocId.hashCode() & 0x7FFFFFFF ); File locFile= new File( credentialsDir, hash ); if ( locFile.exists() ) { if ( !locFile.canRead() ) { logger.log(Level.WARNING, "unable to read file {0}", locFile ); return null; } else { try { String result= FileUtil.readFileToString(locFile).trim(); String[] ss= result.split("\n"); result= ss[1]; return result; } catch (IOException ex) { logger.log( Level.WARNING, ex.getMessage(), ex ); return null; } } } else { return null; } } private void recordCredentialsToKeyChain( Location loc ) { File das2Dir= getDas2UserDirectory(); if ( !das2Dir.exists() ) { if ( !das2Dir.mkdirs() ) { logger.log(Level.WARNING, "unable to mkdir {0}", das2Dir); } } File credentialsDir= new File( das2Dir, "keychain" ); if ( !credentialsDir.exists() ) { if ( !credentialsDir.mkdirs() ) { logger.log(Level.WARNING, "unable to mkdir {0}", credentialsDir); } } String hash= String.format( "%09d.txt", loc.sLocId.hashCode() & 0x7FFFFFFF ); File locFile= new File( credentialsDir, hash ); if ( locFile.exists() && !locFile.canWrite() ) { logger.log(Level.WARNING, "unable to write file {0}", locFile ); } else { try { String credentialsString= loc.sLocId+ "\n" + loc.sUser + ":"+ loc.sPasswd + "\n"; if ( locFile.exists() ) { String old= FileUtil.readFileToString(locFile); if ( old.equals(credentialsString) ) { logger.fine("password didn't change"); return; } } FileUtil.writeStringToFile( locFile, credentialsString ); if ( !locFile.setReadable(false) ) logger.warning("setReadable failure"); if ( !locFile.setReadable(false,false) ) logger.warning("setReadable failure"); if ( !locFile.setReadable(true,true) ) logger.warning("setReadable failure"); if ( !locFile.setWritable(false) ) logger.warning("setWritable failure"); if ( !locFile.setWritable(false,false) ) logger.warning("setWritable failure"); if ( !locFile.setWritable(true,true) ) logger.warning("setWritable failure"); } catch (IOException ex) { logger.log( Level.WARNING, ex.getMessage(), ex ); } } } /** Gather User Credentials * * @param loc The Location in question * @return True if user hit OK, False if user canceled the operation */ protected synchronized boolean getCredentialsGUI(final Location loc) { // Check again to see if another thread managed to set the credentials before // this method started. Need to avoid the double-authenticate dialogs problem // I'm not sure how to prevent the double cancel problem at this time. --cwp if( loc.hasCredentials()) return true; String credentialsFromKeyChain= checkKeyChainForCredentials( loc ); if ( credentialsFromKeyChain!=null ) { int i= credentialsFromKeyChain.indexOf(":"); loc.sUser= credentialsFromKeyChain.substring(0,i); loc.sPasswd= credentialsFromKeyChain.substring(i+1); } try{ SwingUtilities.invokeAndWait( new Runnable(){ @Override public void run(){ // make the dialog if it doesn't exist if(m_dlg == null){ Frame wParent = null; Window[] lTopWnds = Window.getOwnerlessWindows(); for(Window wnd: lTopWnds){ if(wnd.isVisible() && wnd instanceof Frame){ wParent = (Frame)wnd; break; } } m_dlg = new CredentialsDialog(wParent); } String sTmp = loc.sDesc; if((sTmp == null)||(sTmp.isEmpty())) sTmp = loc.sLocId; m_dlg.runDialog(sTmp, loc.iconLogo, loc.sUser, loc.sPasswd); } } ); } catch(InterruptedException ex) { LoggerManager.getLogger("das2.util").severe(ex.toString()); return false; } catch ( InvocationTargetException ex){ LoggerManager.getLogger("das2.util").severe(ex.toString()); return false; } if(m_dlg.getReturn() == JOptionPane.CANCEL_OPTION) return false; loc.sUser = m_dlg.getUser(); loc.sPasswd = m_dlg.getPasswd(); recordCredentialsToKeyChain( loc ); return true; } /** * get the credentials from the command line. * * @param loc * @return */ protected synchronized boolean getCredentialsCmdLine(Location loc){ Console c = System.console(); if(c == null){ throw new IllegalArgumentException("Console is not available to query username and password for " + loc.sDesc); } else{ c.printf("%s\n", loc.sDesc); loc.sUser = c.readLine("Username: "); loc.sPasswd = new String(c.readPassword("Password: ")); return true; } } }