View Javadoc
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  
12  import static java.nio.charset.StandardCharsets.UTF_8;
13  import static java.text.MessageFormat.format;
14  import static org.apache.sshd.client.config.hosts.HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM;
15  import static org.apache.sshd.client.config.hosts.HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM;
16  
17  import java.io.BufferedReader;
18  import java.io.IOException;
19  import java.nio.file.Files;
20  import java.nio.file.Path;
21  import java.util.Arrays;
22  import java.util.Collection;
23  import java.util.LinkedList;
24  import java.util.List;
25  import java.util.stream.Collectors;
26  
27  import org.apache.sshd.client.config.hosts.HostPatternValue;
28  import org.apache.sshd.client.config.hosts.HostPatternsHolder;
29  import org.apache.sshd.client.config.hosts.KnownHostEntry;
30  import org.apache.sshd.client.config.hosts.KnownHostHashValue;
31  import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
32  import org.slf4j.Logger;
33  import org.slf4j.LoggerFactory;
34  
35  /**
36   * Apache MINA sshd 2.0.0 KnownHostEntry cannot read a host entry line like
37   * "host:port ssh-rsa <key>"; it complains about an illegal character in the
38   * host name (correct would be "[host]:port"). The default known_hosts reader
39   * also aborts reading on the first error.
40   * <p>
41   * This reader is a bit more robust and tries to handle this case if there is
42   * only one colon (otherwise it might be an IPv6 address (without port)), and it
43   * skips and logs invalid entries, but still returns all other valid entries
44   * from the file.
45   * </p>
46   */
47  public class KnownHostEntryReader {
48  
49  	private static final Logger LOG = LoggerFactory
50  			.getLogger(KnownHostEntryReader.class);
51  
52  	private KnownHostEntryReader() {
53  		// No instantiation
54  	}
55  
56  	/**
57  	 * Reads a known_hosts file and returns all valid entries. Invalid entries
58  	 * are skipped (and a message is logged).
59  	 *
60  	 * @param path
61  	 *            of the file to read
62  	 * @return a {@link List} of all valid entries read from the file
63  	 * @throws IOException
64  	 *             if the file cannot be read.
65  	 */
66  	public static List<KnownHostEntry> readFromFile(Path path)
67  			throws IOException {
68  		List<KnownHostEntry> result = new LinkedList<>();
69  		try (BufferedReader r = Files.newBufferedReader(path, UTF_8)) {
70  			r.lines().forEachOrdered(l -> {
71  				if (l == null) {
72  					return;
73  				}
74  				String line = clean(l);
75  				if (line.isEmpty()) {
76  					return;
77  				}
78  				try {
79  					KnownHostEntry entry = parseHostEntry(line);
80  					if (entry != null) {
81  						result.add(entry);
82  					} else {
83  						LOG.warn(format(SshdText.get().knownHostsInvalidLine,
84  								path, line));
85  					}
86  				} catch (RuntimeException e) {
87  					LOG.warn(format(SshdText.get().knownHostsInvalidLine, path,
88  							line), e);
89  				}
90  			});
91  		}
92  		return result;
93  	}
94  
95  	private static String clean(String line) {
96  		int i = line.indexOf('#');
97  		return i < 0 ? line.trim() : line.substring(0, i).trim();
98  	}
99  
100 	private static KnownHostEntry parseHostEntry(String line) {
101 		KnownHostEntry entry = new KnownHostEntry();
102 		entry.setConfigLine(line);
103 		String tmp = line;
104 		int i = 0;
105 		if (tmp.charAt(0) == KnownHostEntry.MARKER_INDICATOR) {
106 			// A marker
107 			i = tmp.indexOf(' ', 1);
108 			if (i < 0) {
109 				return null;
110 			}
111 			entry.setMarker(tmp.substring(1, i));
112 			tmp = tmp.substring(i + 1).trim();
113 		}
114 		i = tmp.indexOf(' ');
115 		if (i < 0) {
116 			return null;
117 		}
118 		// Hash, or host patterns
119 		if (tmp.charAt(0) == KnownHostHashValue.HASHED_HOST_DELIMITER) {
120 			// Hashed host entry
121 			KnownHostHashValue hash = KnownHostHashValue
122 					.parse(tmp.substring(0, i));
123 			if (hash == null) {
124 				return null;
125 			}
126 			entry.setHashedEntry(hash);
127 			entry.setPatterns(null);
128 		} else {
129 			Collection<HostPatternValue> patterns = parsePatterns(
130 					tmp.substring(0, i));
131 			if (patterns == null || patterns.isEmpty()) {
132 				return null;
133 			}
134 			entry.setHashedEntry(null);
135 			entry.setPatterns(patterns);
136 		}
137 		tmp = tmp.substring(i + 1).trim();
138 		AuthorizedKeyEntry key = AuthorizedKeyEntry
139 				.parseAuthorizedKeyEntry(tmp);
140 		if (key == null) {
141 			return null;
142 		}
143 		entry.setKeyEntry(key);
144 		return entry;
145 	}
146 
147 	private static Collection<HostPatternValue> parsePatterns(String text) {
148 		if (text.isEmpty()) {
149 			return null;
150 		}
151 		List<String> items = Arrays.stream(text.split(",")) //$NON-NLS-1$
152 				.filter(item -> item != null && !item.isEmpty()).map(item -> {
153 					if (NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM == item
154 							.charAt(0)) {
155 						return item;
156 					}
157 					int firstColon = item.indexOf(':');
158 					if (firstColon < 0) {
159 						return item;
160 					}
161 					int secondColon = item.indexOf(':', firstColon + 1);
162 					if (secondColon > 0) {
163 						// Assume an IPv6 address (without port).
164 						return item;
165 					}
166 					// We have "host:port", should be "[host]:port"
167 					return NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM
168 							+ item.substring(0, firstColon)
169 							+ NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM
170 							+ item.substring(firstColon);
171 				}).collect(Collectors.toList());
172 		return items.isEmpty() ? null : HostPatternsHolder.parsePatterns(items);
173 	}
174 }