/* 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;
}
}
}