KnownHostEntryReader.java

  1. /*
  2.  * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
  3.  * and other copyright owners as documented in the project's IP log.
  4.  *
  5.  * This program and the accompanying materials are made available
  6.  * under the terms of the Eclipse Distribution License v1.0 which
  7.  * accompanies this distribution, is reproduced below, and is
  8.  * available at http://www.eclipse.org/org/documents/edl-v10.php
  9.  *
  10.  * All rights reserved.
  11.  *
  12.  * Redistribution and use in source and binary forms, with or
  13.  * without modification, are permitted provided that the following
  14.  * conditions are met:
  15.  *
  16.  * - Redistributions of source code must retain the above copyright
  17.  *   notice, this list of conditions and the following disclaimer.
  18.  *
  19.  * - Redistributions in binary form must reproduce the above
  20.  *   copyright notice, this list of conditions and the following
  21.  *   disclaimer in the documentation and/or other materials provided
  22.  *   with the distribution.
  23.  *
  24.  * - Neither the name of the Eclipse Foundation, Inc. nor the
  25.  *   names of its contributors may be used to endorse or promote
  26.  *   products derived from this software without specific prior
  27.  *   written permission.
  28.  *
  29.  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  30.  * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  31.  * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  32.  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  33.  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  34.  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  35.  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  36.  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  37.  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  38.  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  39.  * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  40.  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  41.  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  42.  */
  43. package org.eclipse.jgit.internal.transport.sshd;

  44. import static java.nio.charset.StandardCharsets.UTF_8;
  45. import static java.text.MessageFormat.format;
  46. import static org.apache.sshd.client.config.hosts.HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM;
  47. import static org.apache.sshd.client.config.hosts.HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM;

  48. import java.io.BufferedReader;
  49. import java.io.IOException;
  50. import java.nio.file.Files;
  51. import java.nio.file.Path;
  52. import java.util.Arrays;
  53. import java.util.Collection;
  54. import java.util.LinkedList;
  55. import java.util.List;
  56. import java.util.stream.Collectors;

  57. import org.apache.sshd.client.config.hosts.HostPatternValue;
  58. import org.apache.sshd.client.config.hosts.HostPatternsHolder;
  59. import org.apache.sshd.client.config.hosts.KnownHostEntry;
  60. import org.apache.sshd.client.config.hosts.KnownHostHashValue;
  61. import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
  62. import org.slf4j.Logger;
  63. import org.slf4j.LoggerFactory;

  64. /**
  65.  * Apache MINA sshd 2.0.0 KnownHostEntry cannot read a host entry line like
  66.  * "host:port ssh-rsa <key>"; it complains about an illegal character in the
  67.  * host name (correct would be "[host]:port"). The default known_hosts reader
  68.  * also aborts reading on the first error.
  69.  * <p>
  70.  * This reader is a bit more robust and tries to handle this case if there is
  71.  * only one colon (otherwise it might be an IPv6 address (without port)), and it
  72.  * skips and logs invalid entries, but still returns all other valid entries
  73.  * from the file.
  74.  * </p>
  75.  */
  76. public class KnownHostEntryReader {

  77.     private static final Logger LOG = LoggerFactory
  78.             .getLogger(KnownHostEntryReader.class);

  79.     private KnownHostEntryReader() {
  80.         // No instantiation
  81.     }

  82.     /**
  83.      * Reads a known_hosts file and returns all valid entries. Invalid entries
  84.      * are skipped (and a message is logged).
  85.      *
  86.      * @param path
  87.      *            of the file to read
  88.      * @return a {@link List} of all valid entries read from the file
  89.      * @throws IOException
  90.      *             if the file cannot be read.
  91.      */
  92.     public static List<KnownHostEntry> readFromFile(Path path)
  93.             throws IOException {
  94.         List<KnownHostEntry> result = new LinkedList<>();
  95.         try (BufferedReader r = Files.newBufferedReader(path, UTF_8)) {
  96.             r.lines().forEachOrdered(l -> {
  97.                 if (l == null) {
  98.                     return;
  99.                 }
  100.                 String line = clean(l);
  101.                 if (line.isEmpty()) {
  102.                     return;
  103.                 }
  104.                 try {
  105.                     KnownHostEntry entry = parseHostEntry(line);
  106.                     if (entry != null) {
  107.                         result.add(entry);
  108.                     } else {
  109.                         LOG.warn(format(SshdText.get().knownHostsInvalidLine,
  110.                                 path, line));
  111.                     }
  112.                 } catch (RuntimeException e) {
  113.                     LOG.warn(format(SshdText.get().knownHostsInvalidLine, path,
  114.                             line), e);
  115.                 }
  116.             });
  117.         }
  118.         return result;
  119.     }

  120.     private static String clean(String line) {
  121.         int i = line.indexOf('#');
  122.         return i < 0 ? line.trim() : line.substring(0, i).trim();
  123.     }

  124.     private static KnownHostEntry parseHostEntry(String line) {
  125.         KnownHostEntry entry = new KnownHostEntry();
  126.         entry.setConfigLine(line);
  127.         String tmp = line;
  128.         int i = 0;
  129.         if (tmp.charAt(0) == KnownHostEntry.MARKER_INDICATOR) {
  130.             // A marker
  131.             i = tmp.indexOf(' ', 1);
  132.             if (i < 0) {
  133.                 return null;
  134.             }
  135.             entry.setMarker(tmp.substring(1, i));
  136.             tmp = tmp.substring(i + 1).trim();
  137.         }
  138.         i = tmp.indexOf(' ');
  139.         if (i < 0) {
  140.             return null;
  141.         }
  142.         // Hash, or host patterns
  143.         if (tmp.charAt(0) == KnownHostHashValue.HASHED_HOST_DELIMITER) {
  144.             // Hashed host entry
  145.             KnownHostHashValue hash = KnownHostHashValue
  146.                     .parse(tmp.substring(0, i));
  147.             if (hash == null) {
  148.                 return null;
  149.             }
  150.             entry.setHashedEntry(hash);
  151.             entry.setPatterns(null);
  152.         } else {
  153.             Collection<HostPatternValue> patterns = parsePatterns(
  154.                     tmp.substring(0, i));
  155.             if (patterns == null || patterns.isEmpty()) {
  156.                 return null;
  157.             }
  158.             entry.setHashedEntry(null);
  159.             entry.setPatterns(patterns);
  160.         }
  161.         tmp = tmp.substring(i + 1).trim();
  162.         AuthorizedKeyEntry key = AuthorizedKeyEntry
  163.                 .parseAuthorizedKeyEntry(tmp);
  164.         if (key == null) {
  165.             return null;
  166.         }
  167.         entry.setKeyEntry(key);
  168.         return entry;
  169.     }

  170.     private static Collection<HostPatternValue> parsePatterns(String text) {
  171.         if (text.isEmpty()) {
  172.             return null;
  173.         }
  174.         List<String> items = Arrays.stream(text.split(",")) //$NON-NLS-1$
  175.                 .filter(item -> item != null && !item.isEmpty()).map(item -> {
  176.                     if (NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM == item
  177.                             .charAt(0)) {
  178.                         return item;
  179.                     }
  180.                     int firstColon = item.indexOf(':');
  181.                     if (firstColon < 0) {
  182.                         return item;
  183.                     }
  184.                     int secondColon = item.indexOf(':', firstColon + 1);
  185.                     if (secondColon > 0) {
  186.                         // Assume an IPv6 address (without port).
  187.                         return item;
  188.                     }
  189.                     // We have "host:port", should be "[host]:port"
  190.                     return NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM
  191.                             + item.substring(0, firstColon)
  192.                             + NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM
  193.                             + item.substring(firstColon);
  194.                 }).collect(Collectors.toList());
  195.         return items.isEmpty() ? null : HostPatternsHolder.parsePatterns(items);
  196.     }
  197. }