/*
* SSHTools - Java SSH2 API
*
* Copyright (C) 2002-2003 Lee David Painter and Contributors.
*
* Contributions made by:
*
* Brett Smith
* Richard Pernavas
* Erwin Bolwidt
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package com.sshtools.common.hosts;
import com.sshtools.j2ssh.configuration.ConfigurationLoader;
import com.sshtools.j2ssh.transport.HostKeyVerification;
import com.sshtools.j2ssh.transport.InvalidHostFileException;
import com.sshtools.j2ssh.transport.TransportProtocolException;
import com.sshtools.j2ssh.transport.publickey.SshPublicKey;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilePermission;
import java.io.IOException;
import java.io.InputStream;
import java.security.AccessControlException;
import java.security.AccessController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
/**
*
*
* @author $author$
* @version $Revision: 1.13 $
*/
public abstract class AbstractHostKeyVerification extends DefaultHandler
implements HostKeyVerification {
private static String defaultHostFile;
private static Log log = LogFactory.getLog(HostKeyVerification.class);
static {
log.info("Determining default host file");
// Get the sshtools.home system property
defaultHostFile = ConfigurationLoader.getConfigurationDirectory();
if (defaultHostFile == null) {
log.info(
"No configuration location, persistence of host keys will be disabled.");
} else {
// Set the default host file name to our hosts.xml
defaultHostFile += "hosts.xml";
log.info("Defaulting host file to " + defaultHostFile);
}
}
private List deniedHosts = new ArrayList();
private Map allowedHosts = new HashMap();
private String hostFile;
private boolean hostFileWriteable;
private boolean expectEndElement = false;
private String currentElement = null;
/**
* Creates a new AbstractHostKeyVerification object.
*
* @throws InvalidHostFileException
*/
public AbstractHostKeyVerification() throws InvalidHostFileException {
this(defaultHostFile);
hostFile = defaultHostFile;
}
/**
* Creates a new AbstractHostKeyVerification object.
*
* @param hostFileName
*
* @throws InvalidHostFileException
*/
public AbstractHostKeyVerification(String hostFileName)
throws InvalidHostFileException {
InputStream in = null;
try {
// If no host file is supplied, or there is not enough permission to load
// the file, then just create an empty list.
if (hostFileName != null) {
if (System.getSecurityManager() != null) {
AccessController.checkPermission(new FilePermission(
hostFileName, "read"));
}
// Load the hosts file. Do not worry if fle doesnt exist, just disable
// save of
File f = new File(hostFileName);
if (f.exists()) {
in = new FileInputStream(f);
hostFile = hostFileName;
SAXParserFactory saxFactory = SAXParserFactory.newInstance();
SAXParser saxParser = saxFactory.newSAXParser();
saxParser.parse(in, this);
hostFileWriteable = f.canWrite();
} else {
// Try to create the file
if (f.createNewFile()) {
FileOutputStream out = new FileOutputStream(f);
out.write(toString().getBytes());
out.close();
hostFileWriteable = true;
} else {
hostFileWriteable = false;
}
}
if (!hostFileWriteable) {
log.warn("Host file is not writeable.");
}
}
} catch (AccessControlException ace) {
log.warn(
"Not enough permission to load a hosts file, so just creating an empty list");
} catch (IOException ioe) {
throw new InvalidHostFileException("Could not open or read " +
hostFileName);
} catch (SAXException sax) {
throw new InvalidHostFileException("Failed XML parsing: " +
sax.getMessage());
} catch (ParserConfigurationException pce) {
throw new InvalidHostFileException(
"Failed to initialize xml parser: " + pce.getMessage());
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ioe) {
}
}
}
}
/**
*
*
* @param uri
* @param localName
* @param qname
* @param attrs
*
* @throws SAXException
*/
public void startElement(String uri, String localName, String qname,
Attributes attrs) throws SAXException {
if (currentElement == null) {
if (qname.equals("HostAuthorizations")) {
allowedHosts.clear();
deniedHosts.clear();
currentElement = qname;
} else {
throw new SAXException("Unexpected document element!");
}
} else {
if (!currentElement.equals("HostAuthorizations")) {
throw new SAXException("Unexpected parent element found!");
}
if (qname.equals("AllowHost")) {
String hostname = attrs.getValue("HostName");
String fingerprint = attrs.getValue("Fingerprint");
if ((hostname != null) && (fingerprint != null)) {
if (log.isDebugEnabled()) {
log.debug("AllowHost element for host '" + hostname +
"' with fingerprint '" + fingerprint + "'");
}
allowedHosts.put(hostname, fingerprint);
currentElement = qname;
} else {
throw new SAXException("Requried attribute(s) missing!");
}
} else if (qname.equals("DenyHost")) {
String hostname = attrs.getValue("HostName");
if (hostname != null) {
if (log.isDebugEnabled()) {
log.debug("DenyHost element for host " + hostname);
}
deniedHosts.add(hostname);
currentElement = qname;
} else {
throw new SAXException(
"Required attribute hostname missing");
}
} else {
log.warn("Unexpected " + qname +
" element found in allowed hosts file");
}
}
}
/**
*
*
* @param uri
* @param localName
* @param qname
*
* @throws SAXException
*/
public void endElement(String uri, String localName, String qname)
throws SAXException {
if (currentElement == null) {
throw new SAXException("Unexpected end element found!");
}
if (currentElement.equals("HostAuthorizations")) {
currentElement = null;
return;
}
if (currentElement.equals("AllowHost")) {
currentElement = "HostAuthorizations";
return;
}
if (currentElement.equals("DenyHost")) {
currentElement = "HostAuthorizations";
return;
}
}
/**
*
*
* @return
*/
public boolean isHostFileWriteable() {
return hostFileWriteable;
}
/**
*
*
* @param host
*
* @throws TransportProtocolException
*/
public abstract void onDeniedHost(String host)
throws TransportProtocolException;
/**
*
*
* @param host
* @param allowedHostKey
* @param actualHostKey
*
* @throws TransportProtocolException
*/
public abstract void onHostKeyMismatch(String host, String allowedHostKey,
String actualHostKey) throws TransportProtocolException;
/**
*
*
* @param host
* @param hostKeyFingerprint
*
* @throws TransportProtocolException
*/
public abstract void onUnknownHost(String host, String hostKeyFingerprint)
throws TransportProtocolException;
/**
*
*
* @param host
* @param hostKeyFingerprint
* @param always
*
* @throws InvalidHostFileException
*/
public void allowHost(String host, String hostKeyFingerprint, boolean always)
throws InvalidHostFileException {
if (log.isDebugEnabled()) {
log.debug("Allowing " + host + " with fingerprint " +
hostKeyFingerprint);
}
// Put the host into the allowed hosts list, overiding any previous
// entry
allowedHosts.put(host, hostKeyFingerprint);
// If we always want to allow then save the host file with the
// new details
if (always) {
saveHostFile();
}
}
/**
*
*
* @return
*/
public Map allowedHosts() {
return allowedHosts;
}
/**
*
*
* @return
*/
public java.util.List deniedHosts() {
return deniedHosts;
}
/**
*
*
* @param host
*/
public void removeAllowedHost(String host) {
allowedHosts.remove(host);
}
/**
*
*
* @param host
*/
public void removeDeniedHost(String host) {
for (int i = deniedHosts.size() - 1; i >= 0; i--) {
String h = (String) deniedHosts.get(i);
if (h.equals(host)) {
deniedHosts.remove(i);
}
}
}
/**
*
*
* @param host
* @param always
*
* @throws InvalidHostFileException
*/
public void denyHost(String host, boolean always)
throws InvalidHostFileException {
if (log.isDebugEnabled()) {
log.debug(host + " is denied access");
}
// Get the denied host from the list
if (!deniedHosts.contains(host)) {
deniedHosts.add(host);
}
// Save it if need be
if (always) {
saveHostFile();
}
}
/**
*
*
* @param host
* @param pk
*
* @return
*
* @throws TransportProtocolException
*/
public boolean verifyHost(String host, SshPublicKey pk)
throws TransportProtocolException {
String fingerprint = pk.getFingerprint();
log.info("Verifying " + host + " host key");
if (log.isDebugEnabled()) {
log.debug("Fingerprint: " + fingerprint);
}
// See if the host is denied by looking at the denied hosts list
if (deniedHosts.contains(host)) {
onDeniedHost(host);
return false;
}
// Try the allowed hosts by looking at the allowed hosts map
if (allowedHosts.containsKey(host)) {
// The host is allowed so check the fingerprint
String currentFingerprint = (String) allowedHosts.get(host);
if (currentFingerprint.compareToIgnoreCase(fingerprint) == 0) {
return true;
}
// The host key does not match the recorded so call the abstract
// method so that the user can decide
onHostKeyMismatch(host, currentFingerprint, fingerprint);
// Recheck the after the users input
return checkFingerprint(host, fingerprint);
} else {
// The host is unknown os ask the user
onUnknownHost(host, fingerprint);
// Recheck ans return the result
return checkFingerprint(host, fingerprint);
}
}
private boolean checkFingerprint(String host, String fingerprint) {
String currentFingerprint = (String) allowedHosts.get(host);
if (currentFingerprint != null) {
if (currentFingerprint.compareToIgnoreCase(fingerprint) == 0) {
return true;
}
}
return false;
}
/**
*
*
* @throws InvalidHostFileException
*/
public void saveHostFile() throws InvalidHostFileException {
if (!hostFileWriteable) {
throw new InvalidHostFileException("Host file is not writeable.");
}
log.info("Saving " + defaultHostFile);
try {
File f = new File(hostFile);
FileOutputStream out = new FileOutputStream(f);
out.write(toString().getBytes());
out.close();
} catch (IOException e) {
throw new InvalidHostFileException("Could not write to " +
hostFile);
}
}
/**
*
*
* @return
*/
public String toString() {
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<HostAuthorizations>\n";
xml += "<!-- Host Authorizations file, used by the abstract class HostKeyVerification to verify the servers host key -->";
xml += " <!-- Allow the following hosts access if they provide the correct public key -->\n";
Map.Entry entry;
Iterator it = allowedHosts.entrySet().iterator();
while (it.hasNext()) {
entry = (Map.Entry) it.next();
xml += (" " + "<AllowHost HostName=\"" +
entry.getKey().toString() + "\" Fingerprint=\"" +
entry.getValue().toString() + "\"/>\n");
}
xml += " <!-- Deny the following hosts access -->\n";
it = deniedHosts.iterator();
while (it.hasNext()) {
xml += (" <DenyHost HostName=\"" + it.next().toString() +
"\"/>\n");
}
xml += "</HostAuthorizations>";
return xml;
}
}