KnownHostEntryReader.java

  1. /*
  2.  * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */
  10. package org.eclipse.jgit.internal.transport.sshd;

  11. import static java.nio.charset.StandardCharsets.UTF_8;
  12. import static java.text.MessageFormat.format;
  13. import static org.apache.sshd.client.config.hosts.HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM;
  14. import static org.apache.sshd.client.config.hosts.HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM;

  15. import java.io.BufferedReader;
  16. import java.io.IOException;
  17. import java.nio.file.Files;
  18. import java.nio.file.Path;
  19. import java.util.Arrays;
  20. import java.util.Collection;
  21. import java.util.LinkedList;
  22. import java.util.List;
  23. import java.util.stream.Collectors;

  24. import org.apache.sshd.client.config.hosts.HostPatternValue;
  25. import org.apache.sshd.client.config.hosts.HostPatternsHolder;
  26. import org.apache.sshd.client.config.hosts.KnownHostEntry;
  27. import org.apache.sshd.client.config.hosts.KnownHostHashValue;
  28. import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
  29. import org.slf4j.Logger;
  30. import org.slf4j.LoggerFactory;

  31. /**
  32.  * Apache MINA sshd 2.0.0 KnownHostEntry cannot read a host entry line like
  33.  * "host:port ssh-rsa <key>"; it complains about an illegal character in the
  34.  * host name (correct would be "[host]:port"). The default known_hosts reader
  35.  * also aborts reading on the first error.
  36.  * <p>
  37.  * This reader is a bit more robust and tries to handle this case if there is
  38.  * only one colon (otherwise it might be an IPv6 address (without port)), and it
  39.  * skips and logs invalid entries, but still returns all other valid entries
  40.  * from the file.
  41.  * </p>
  42.  */
  43. public class KnownHostEntryReader {

  44.     private static final Logger LOG = LoggerFactory
  45.             .getLogger(KnownHostEntryReader.class);

  46.     private KnownHostEntryReader() {
  47.         // No instantiation
  48.     }

  49.     /**
  50.      * Reads a known_hosts file and returns all valid entries. Invalid entries
  51.      * are skipped (and a message is logged).
  52.      *
  53.      * @param path
  54.      *            of the file to read
  55.      * @return a {@link List} of all valid entries read from the file
  56.      * @throws IOException
  57.      *             if the file cannot be read.
  58.      */
  59.     public static List<KnownHostEntry> readFromFile(Path path)
  60.             throws IOException {
  61.         List<KnownHostEntry> result = new LinkedList<>();
  62.         try (BufferedReader r = Files.newBufferedReader(path, UTF_8)) {
  63.             r.lines().forEachOrdered(l -> {
  64.                 if (l == null) {
  65.                     return;
  66.                 }
  67.                 String line = clean(l);
  68.                 if (line.isEmpty()) {
  69.                     return;
  70.                 }
  71.                 try {
  72.                     KnownHostEntry entry = parseHostEntry(line);
  73.                     if (entry != null) {
  74.                         result.add(entry);
  75.                     } else {
  76.                         LOG.warn(format(SshdText.get().knownHostsInvalidLine,
  77.                                 path, line));
  78.                     }
  79.                 } catch (RuntimeException e) {
  80.                     LOG.warn(format(SshdText.get().knownHostsInvalidLine, path,
  81.                             line), e);
  82.                 }
  83.             });
  84.         }
  85.         return result;
  86.     }

  87.     private static String clean(String line) {
  88.         int i = line.indexOf('#');
  89.         return i < 0 ? line.trim() : line.substring(0, i).trim();
  90.     }

  91.     private static KnownHostEntry parseHostEntry(String line) {
  92.         KnownHostEntry entry = new KnownHostEntry();
  93.         entry.setConfigLine(line);
  94.         String tmp = line;
  95.         int i = 0;
  96.         if (tmp.charAt(0) == KnownHostEntry.MARKER_INDICATOR) {
  97.             // A marker
  98.             i = tmp.indexOf(' ', 1);
  99.             if (i < 0) {
  100.                 return null;
  101.             }
  102.             entry.setMarker(tmp.substring(1, i));
  103.             tmp = tmp.substring(i + 1).trim();
  104.         }
  105.         i = tmp.indexOf(' ');
  106.         if (i < 0) {
  107.             return null;
  108.         }
  109.         // Hash, or host patterns
  110.         if (tmp.charAt(0) == KnownHostHashValue.HASHED_HOST_DELIMITER) {
  111.             // Hashed host entry
  112.             KnownHostHashValue hash = KnownHostHashValue
  113.                     .parse(tmp.substring(0, i));
  114.             if (hash == null) {
  115.                 return null;
  116.             }
  117.             entry.setHashedEntry(hash);
  118.             entry.setPatterns(null);
  119.         } else {
  120.             Collection<HostPatternValue> patterns = parsePatterns(
  121.                     tmp.substring(0, i));
  122.             if (patterns == null || patterns.isEmpty()) {
  123.                 return null;
  124.             }
  125.             entry.setHashedEntry(null);
  126.             entry.setPatterns(patterns);
  127.         }
  128.         tmp = tmp.substring(i + 1).trim();
  129.         AuthorizedKeyEntry key = AuthorizedKeyEntry
  130.                 .parseAuthorizedKeyEntry(tmp);
  131.         if (key == null) {
  132.             return null;
  133.         }
  134.         entry.setKeyEntry(key);
  135.         return entry;
  136.     }

  137.     private static Collection<HostPatternValue> parsePatterns(String text) {
  138.         if (text.isEmpty()) {
  139.             return null;
  140.         }
  141.         List<String> items = Arrays.stream(text.split(",")) //$NON-NLS-1$
  142.                 .filter(item -> item != null && !item.isEmpty()).map(item -> {
  143.                     if (NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM == item
  144.                             .charAt(0)) {
  145.                         return item;
  146.                     }
  147.                     int firstColon = item.indexOf(':');
  148.                     if (firstColon < 0) {
  149.                         return item;
  150.                     }
  151.                     int secondColon = item.indexOf(':', firstColon + 1);
  152.                     if (secondColon > 0) {
  153.                         // Assume an IPv6 address (without port).
  154.                         return item;
  155.                     }
  156.                     // We have "host:port", should be "[host]:port"
  157.                     return NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM
  158.                             + item.substring(0, firstColon)
  159.                             + NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM
  160.                             + item.substring(firstColon);
  161.                 }).collect(Collectors.toList());
  162.         return items.isEmpty() ? null : HostPatternsHolder.parsePatterns(items);
  163.     }
  164. }