001: /*
002: * SSHTools - Java SSH2 API
003: *
004: * Copyright (C) 2002-2003 Lee David Painter and Contributors.
005: *
006: * Contributions made by:
007: *
008: * Brett Smith
009: * Richard Pernavas
010: * Erwin Bolwidt
011: *
012: * This program is free software; you can redistribute it and/or
013: * modify it under the terms of the GNU General Public License
014: * as published by the Free Software Foundation; either version 2
015: * of the License, or (at your option) any later version.
016: *
017: * This program is distributed in the hope that it will be useful,
018: * but WITHOUT ANY WARRANTY; without even the implied warranty of
019: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
020: * GNU General Public License for more details.
021: *
022: * You should have received a copy of the GNU General Public License
023: * along with this program; if not, write to the Free Software
024: * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
025: */
026: package com.sshtools.common.hosts;
027:
028: import com.sshtools.j2ssh.configuration.ConfigurationLoader;
029: import com.sshtools.j2ssh.transport.HostKeyVerification;
030: import com.sshtools.j2ssh.transport.InvalidHostFileException;
031: import com.sshtools.j2ssh.transport.TransportProtocolException;
032: import com.sshtools.j2ssh.transport.publickey.SshPublicKey;
033:
034: import org.apache.commons.logging.Log;
035: import org.apache.commons.logging.LogFactory;
036:
037: import org.xml.sax.Attributes;
038: import org.xml.sax.SAXException;
039: import org.xml.sax.helpers.DefaultHandler;
040:
041: import java.io.File;
042: import java.io.FileInputStream;
043: import java.io.FileOutputStream;
044: import java.io.FilePermission;
045: import java.io.IOException;
046: import java.io.InputStream;
047:
048: import java.security.AccessControlException;
049: import java.security.AccessController;
050:
051: import java.util.ArrayList;
052: import java.util.HashMap;
053: import java.util.Iterator;
054: import java.util.List;
055: import java.util.Map;
056:
057: import javax.xml.parsers.ParserConfigurationException;
058: import javax.xml.parsers.SAXParser;
059: import javax.xml.parsers.SAXParserFactory;
060:
061: /**
062: *
063: *
064: * @author $author$
065: * @version $Revision: 1.13 $
066: */
067: public abstract class AbstractHostKeyVerification extends
068: DefaultHandler implements HostKeyVerification {
069: private static String defaultHostFile;
070: private static Log log = LogFactory
071: .getLog(HostKeyVerification.class);
072:
073: static {
074: log.info("Determining default host file");
075:
076: // Get the sshtools.home system property
077: defaultHostFile = ConfigurationLoader
078: .getConfigurationDirectory();
079:
080: if (defaultHostFile == null) {
081: log
082: .info("No configuration location, persistence of host keys will be disabled.");
083: } else {
084: // Set the default host file name to our hosts.xml
085: defaultHostFile += "hosts.xml";
086: log.info("Defaulting host file to " + defaultHostFile);
087: }
088: }
089:
090: private List deniedHosts = new ArrayList();
091: private Map allowedHosts = new HashMap();
092: private String hostFile;
093: private boolean hostFileWriteable;
094: private boolean expectEndElement = false;
095: private String currentElement = null;
096:
097: /**
098: * Creates a new AbstractHostKeyVerification object.
099: *
100: * @throws InvalidHostFileException
101: */
102: public AbstractHostKeyVerification()
103: throws InvalidHostFileException {
104: this (defaultHostFile);
105: hostFile = defaultHostFile;
106: }
107:
108: /**
109: * Creates a new AbstractHostKeyVerification object.
110: *
111: * @param hostFileName
112: *
113: * @throws InvalidHostFileException
114: */
115: public AbstractHostKeyVerification(String hostFileName)
116: throws InvalidHostFileException {
117: InputStream in = null;
118:
119: try {
120: // If no host file is supplied, or there is not enough permission to load
121: // the file, then just create an empty list.
122: if (hostFileName != null) {
123: if (System.getSecurityManager() != null) {
124: AccessController
125: .checkPermission(new FilePermission(
126: hostFileName, "read"));
127: }
128:
129: // Load the hosts file. Do not worry if fle doesnt exist, just disable
130: // save of
131: File f = new File(hostFileName);
132:
133: if (f.exists()) {
134: in = new FileInputStream(f);
135: hostFile = hostFileName;
136:
137: SAXParserFactory saxFactory = SAXParserFactory
138: .newInstance();
139: SAXParser saxParser = saxFactory.newSAXParser();
140: saxParser.parse(in, this );
141: hostFileWriteable = f.canWrite();
142: } else {
143: // Try to create the file
144: if (f.createNewFile()) {
145: FileOutputStream out = new FileOutputStream(f);
146: out.write(toString().getBytes());
147: out.close();
148: hostFileWriteable = true;
149: } else {
150: hostFileWriteable = false;
151: }
152: }
153:
154: if (!hostFileWriteable) {
155: log.warn("Host file is not writeable.");
156: }
157: }
158: } catch (AccessControlException ace) {
159: log
160: .warn("Not enough permission to load a hosts file, so just creating an empty list");
161: } catch (IOException ioe) {
162: throw new InvalidHostFileException(
163: "Could not open or read " + hostFileName);
164: } catch (SAXException sax) {
165: throw new InvalidHostFileException("Failed XML parsing: "
166: + sax.getMessage());
167: } catch (ParserConfigurationException pce) {
168: throw new InvalidHostFileException(
169: "Failed to initialize xml parser: "
170: + pce.getMessage());
171: } finally {
172: if (in != null) {
173: try {
174: in.close();
175: } catch (IOException ioe) {
176: }
177: }
178: }
179: }
180:
181: /**
182: *
183: *
184: * @param uri
185: * @param localName
186: * @param qname
187: * @param attrs
188: *
189: * @throws SAXException
190: */
191: public void startElement(String uri, String localName,
192: String qname, Attributes attrs) throws SAXException {
193: if (currentElement == null) {
194: if (qname.equals("HostAuthorizations")) {
195: allowedHosts.clear();
196: deniedHosts.clear();
197: currentElement = qname;
198: } else {
199: throw new SAXException("Unexpected document element!");
200: }
201: } else {
202: if (!currentElement.equals("HostAuthorizations")) {
203: throw new SAXException(
204: "Unexpected parent element found!");
205: }
206:
207: if (qname.equals("AllowHost")) {
208: String hostname = attrs.getValue("HostName");
209: String fingerprint = attrs.getValue("Fingerprint");
210:
211: if ((hostname != null) && (fingerprint != null)) {
212: if (log.isDebugEnabled()) {
213: log.debug("AllowHost element for host '"
214: + hostname + "' with fingerprint '"
215: + fingerprint + "'");
216: }
217:
218: allowedHosts.put(hostname, fingerprint);
219: currentElement = qname;
220: } else {
221: throw new SAXException(
222: "Requried attribute(s) missing!");
223: }
224: } else if (qname.equals("DenyHost")) {
225: String hostname = attrs.getValue("HostName");
226:
227: if (hostname != null) {
228: if (log.isDebugEnabled()) {
229: log.debug("DenyHost element for host "
230: + hostname);
231: }
232:
233: deniedHosts.add(hostname);
234: currentElement = qname;
235: } else {
236: throw new SAXException(
237: "Required attribute hostname missing");
238: }
239: } else {
240: log.warn("Unexpected " + qname
241: + " element found in allowed hosts file");
242: }
243: }
244: }
245:
246: /**
247: *
248: *
249: * @param uri
250: * @param localName
251: * @param qname
252: *
253: * @throws SAXException
254: */
255: public void endElement(String uri, String localName, String qname)
256: throws SAXException {
257: if (currentElement == null) {
258: throw new SAXException("Unexpected end element found!");
259: }
260:
261: if (currentElement.equals("HostAuthorizations")) {
262: currentElement = null;
263:
264: return;
265: }
266:
267: if (currentElement.equals("AllowHost")) {
268: currentElement = "HostAuthorizations";
269:
270: return;
271: }
272:
273: if (currentElement.equals("DenyHost")) {
274: currentElement = "HostAuthorizations";
275:
276: return;
277: }
278: }
279:
280: /**
281: *
282: *
283: * @return
284: */
285: public boolean isHostFileWriteable() {
286: return hostFileWriteable;
287: }
288:
289: /**
290: *
291: *
292: * @param host
293: *
294: * @throws TransportProtocolException
295: */
296: public abstract void onDeniedHost(String host)
297: throws TransportProtocolException;
298:
299: /**
300: *
301: *
302: * @param host
303: * @param allowedHostKey
304: * @param actualHostKey
305: *
306: * @throws TransportProtocolException
307: */
308: public abstract void onHostKeyMismatch(String host,
309: String allowedHostKey, String actualHostKey)
310: throws TransportProtocolException;
311:
312: /**
313: *
314: *
315: * @param host
316: * @param hostKeyFingerprint
317: *
318: * @throws TransportProtocolException
319: */
320: public abstract void onUnknownHost(String host,
321: String hostKeyFingerprint)
322: throws TransportProtocolException;
323:
324: /**
325: *
326: *
327: * @param host
328: * @param hostKeyFingerprint
329: * @param always
330: *
331: * @throws InvalidHostFileException
332: */
333: public void allowHost(String host, String hostKeyFingerprint,
334: boolean always) throws InvalidHostFileException {
335: if (log.isDebugEnabled()) {
336: log.debug("Allowing " + host + " with fingerprint "
337: + hostKeyFingerprint);
338: }
339:
340: // Put the host into the allowed hosts list, overiding any previous
341: // entry
342: allowedHosts.put(host, hostKeyFingerprint);
343:
344: // If we always want to allow then save the host file with the
345: // new details
346: if (always) {
347: saveHostFile();
348: }
349: }
350:
351: /**
352: *
353: *
354: * @return
355: */
356: public Map allowedHosts() {
357: return allowedHosts;
358: }
359:
360: /**
361: *
362: *
363: * @return
364: */
365: public java.util.List deniedHosts() {
366: return deniedHosts;
367: }
368:
369: /**
370: *
371: *
372: * @param host
373: */
374: public void removeAllowedHost(String host) {
375: allowedHosts.remove(host);
376: }
377:
378: /**
379: *
380: *
381: * @param host
382: */
383: public void removeDeniedHost(String host) {
384: for (int i = deniedHosts.size() - 1; i >= 0; i--) {
385: String h = (String) deniedHosts.get(i);
386:
387: if (h.equals(host)) {
388: deniedHosts.remove(i);
389: }
390: }
391: }
392:
393: /**
394: *
395: *
396: * @param host
397: * @param always
398: *
399: * @throws InvalidHostFileException
400: */
401: public void denyHost(String host, boolean always)
402: throws InvalidHostFileException {
403: if (log.isDebugEnabled()) {
404: log.debug(host + " is denied access");
405: }
406:
407: // Get the denied host from the list
408: if (!deniedHosts.contains(host)) {
409: deniedHosts.add(host);
410: }
411:
412: // Save it if need be
413: if (always) {
414: saveHostFile();
415: }
416: }
417:
418: /**
419: *
420: *
421: * @param host
422: * @param pk
423: *
424: * @return
425: *
426: * @throws TransportProtocolException
427: */
428: public boolean verifyHost(String host, SshPublicKey pk)
429: throws TransportProtocolException {
430: String fingerprint = pk.getFingerprint();
431: log.info("Verifying " + host + " host key");
432:
433: if (log.isDebugEnabled()) {
434: log.debug("Fingerprint: " + fingerprint);
435: }
436:
437: // See if the host is denied by looking at the denied hosts list
438: if (deniedHosts.contains(host)) {
439: onDeniedHost(host);
440:
441: return false;
442: }
443:
444: // Try the allowed hosts by looking at the allowed hosts map
445: if (allowedHosts.containsKey(host)) {
446: // The host is allowed so check the fingerprint
447: String currentFingerprint = (String) allowedHosts.get(host);
448:
449: if (currentFingerprint.compareToIgnoreCase(fingerprint) == 0) {
450: return true;
451: }
452:
453: // The host key does not match the recorded so call the abstract
454: // method so that the user can decide
455: onHostKeyMismatch(host, currentFingerprint, fingerprint);
456:
457: // Recheck the after the users input
458: return checkFingerprint(host, fingerprint);
459: } else {
460: // The host is unknown os ask the user
461: onUnknownHost(host, fingerprint);
462:
463: // Recheck ans return the result
464: return checkFingerprint(host, fingerprint);
465: }
466: }
467:
468: private boolean checkFingerprint(String host, String fingerprint) {
469: String currentFingerprint = (String) allowedHosts.get(host);
470:
471: if (currentFingerprint != null) {
472: if (currentFingerprint.compareToIgnoreCase(fingerprint) == 0) {
473: return true;
474: }
475: }
476:
477: return false;
478: }
479:
480: /**
481: *
482: *
483: * @throws InvalidHostFileException
484: */
485: public void saveHostFile() throws InvalidHostFileException {
486: if (!hostFileWriteable) {
487: throw new InvalidHostFileException(
488: "Host file is not writeable.");
489: }
490:
491: log.info("Saving " + defaultHostFile);
492:
493: try {
494: File f = new File(hostFile);
495: FileOutputStream out = new FileOutputStream(f);
496: out.write(toString().getBytes());
497: out.close();
498: } catch (IOException e) {
499: throw new InvalidHostFileException("Could not write to "
500: + hostFile);
501: }
502: }
503:
504: /**
505: *
506: *
507: * @return
508: */
509: public String toString() {
510: String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<HostAuthorizations>\n";
511: xml += "<!-- Host Authorizations file, used by the abstract class HostKeyVerification to verify the servers host key -->";
512: xml += " <!-- Allow the following hosts access if they provide the correct public key -->\n";
513:
514: Map.Entry entry;
515: Iterator it = allowedHosts.entrySet().iterator();
516:
517: while (it.hasNext()) {
518: entry = (Map.Entry) it.next();
519: xml += (" " + "<AllowHost HostName=\""
520: + entry.getKey().toString() + "\" Fingerprint=\""
521: + entry.getValue().toString() + "\"/>\n");
522: }
523:
524: xml += " <!-- Deny the following hosts access -->\n";
525: it = deniedHosts.iterator();
526:
527: while (it.hasNext()) {
528: xml += (" <DenyHost HostName=\"" + it.next().toString() + "\"/>\n");
529: }
530:
531: xml += "</HostAuthorizations>";
532:
533: return xml;
534: }
535: }
|