Add support for ~/.ssh/config PreferredAuthentications
[egit.git] / org.spearce.jgit / src / org / spearce / jgit / transport / OpenSshConfig.java
blob2f41e562c42e9f5386a70a44098b61e9c486d4f3
1 /*
2 * Copyright (C) 2008, Google Inc.
4 * All rights reserved.
6 * Redistribution and use in source and binary forms, with or
7 * without modification, are permitted provided that the following
8 * conditions are met:
10 * - Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
13 * - Redistributions in binary form must reproduce the above
14 * copyright notice, this list of conditions and the following
15 * disclaimer in the documentation and/or other materials provided
16 * with the distribution.
18 * - Neither the name of the Git Development Community nor the
19 * names of its contributors may be used to endorse or promote
20 * products derived from this software without specific prior
21 * written permission.
23 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
24 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
25 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
26 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
27 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
28 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
29 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
30 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
31 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
32 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
33 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
34 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
35 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38 package org.spearce.jgit.transport;
40 import java.io.BufferedReader;
41 import java.io.File;
42 import java.io.FileInputStream;
43 import java.io.FileNotFoundException;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.io.InputStreamReader;
47 import java.util.ArrayList;
48 import java.util.Collections;
49 import java.util.LinkedHashMap;
50 import java.util.List;
51 import java.util.Map;
53 import org.spearce.jgit.errors.InvalidPatternException;
54 import org.spearce.jgit.fnmatch.FileNameMatcher;
55 import org.spearce.jgit.util.FS;
57 /**
58 * Simple configuration parser for the OpenSSH ~/.ssh/config file.
59 * <p>
60 * Since JSch does not (currently) have the ability to parse an OpenSSH
61 * configuration file this is a simple parser to read that file and make the
62 * critical options available to {@link SshSessionFactory}.
64 public class OpenSshConfig {
65 /**
66 * Obtain the user's configuration data.
67 * <p>
68 * The configuration file is always returned to the caller, even if no file
69 * exists in the user's home directory at the time the call was made. Lookup
70 * requests are cached and are automatically updated if the user modifies
71 * the configuration file since the last time it was cached.
73 * @return a caching reader of the user's configuration file.
75 public static OpenSshConfig get() {
76 File home = FS.userHome();
77 if (home == null)
78 home = new File(".").getAbsoluteFile();
80 final File config = new File(new File(home, ".ssh"), "config");
81 final OpenSshConfig osc = new OpenSshConfig(home, config);
82 osc.refresh();
83 return osc;
86 /** The user's home directory, as key files may be relative to here. */
87 private final File home;
89 /** The .ssh/config file we read and monitor for updates. */
90 private final File configFile;
92 /** Modification time of {@link #configFile} when {@link #hosts} loaded. */
93 private long lastModified;
95 /** Cached entries read out of the configuration file. */
96 private Map<String, Host> hosts;
98 protected OpenSshConfig(final File h, final File cfg) {
99 home = h;
100 configFile = cfg;
101 hosts = Collections.emptyMap();
105 * Locate the configuration for a specific host request.
107 * @param hostName
108 * the name the user has supplied to the SSH tool. This may be a
109 * real host name, or it may just be a "Host" block in the
110 * configuration file.
111 * @return r configuration for the requested name. Never null.
113 public Host lookup(final String hostName) {
114 final Map<String, Host> cache = refresh();
115 Host h = cache.get(hostName);
116 if (h == null)
117 h = new Host();
118 if (h.patternsApplied)
119 return h;
121 for (final Map.Entry<String, Host> e : cache.entrySet()) {
122 if (!isHostPattern(e.getKey()))
123 continue;
124 if (!isHostMatch(e.getKey(), hostName))
125 continue;
126 h.copyFrom(e.getValue());
129 if (h.hostName == null)
130 h.hostName = hostName;
131 if (h.user == null)
132 h.user = DefaultSshSessionFactory.userName();
133 if (h.port == 0)
134 h.port = DefaultSshSessionFactory.SSH_PORT;
135 h.patternsApplied = true;
136 return h;
139 private synchronized Map<String, Host> refresh() {
140 final long mtime = configFile.lastModified();
141 if (mtime != lastModified) {
142 try {
143 final FileInputStream in = new FileInputStream(configFile);
144 try {
145 hosts = parse(in);
146 } finally {
147 in.close();
149 } catch (FileNotFoundException none) {
150 hosts = Collections.emptyMap();
151 } catch (IOException err) {
152 hosts = Collections.emptyMap();
154 lastModified = mtime;
156 return hosts;
159 private Map<String, Host> parse(final InputStream in) throws IOException {
160 final Map<String, Host> m = new LinkedHashMap<String, Host>();
161 final BufferedReader br = new BufferedReader(new InputStreamReader(in));
162 final List<Host> current = new ArrayList<Host>(4);
163 String line;
165 while ((line = br.readLine()) != null) {
166 line = line.trim();
167 if (line.length() == 0 || line.startsWith("#"))
168 continue;
170 final int sp = line.indexOf(' ');
171 final int eq = line.indexOf('=');
172 final int splitAt;
173 if (sp >= 0 && eq >= 0)
174 splitAt = Math.min(sp, eq);
175 else if (sp < 0)
176 splitAt = eq;
177 else
178 splitAt = sp;
179 final String keyword = line.substring(0, splitAt).trim();
180 final String argValue = line.substring(splitAt + 1).trim();
182 if ("Host".equalsIgnoreCase(keyword)) {
183 current.clear();
184 for (final String name : argValue.split("[ \t]")) {
185 Host c = m.get(name);
186 if (c == null) {
187 c = new Host();
188 m.put(name, c);
190 current.add(c);
192 continue;
195 if (current.isEmpty()) {
196 // We received an option outside of a Host block. We
197 // don't know who this should match against, so skip.
199 continue;
202 if ("HostName".equalsIgnoreCase(keyword)) {
203 for (final Host c : current)
204 if (c.hostName == null)
205 c.hostName = dequote(argValue);
206 } else if ("User".equalsIgnoreCase(keyword)) {
207 for (final Host c : current)
208 if (c.user == null)
209 c.user = dequote(argValue);
210 } else if ("Port".equalsIgnoreCase(keyword)) {
211 try {
212 final int port = Integer.parseInt(dequote(argValue));
213 for (final Host c : current)
214 if (c.port == 0)
215 c.port = port;
216 } catch (NumberFormatException nfe) {
217 // Bad port number. Don't set it.
219 } else if ("IdentityFile".equalsIgnoreCase(keyword)) {
220 for (final Host c : current)
221 if (c.identityFile == null)
222 c.identityFile = toFile(dequote(argValue));
223 } else if ("PreferredAuthentications".equalsIgnoreCase(keyword)) {
224 for (final Host c : current)
225 if (c.preferredAuthentications == null)
226 c.preferredAuthentications = nows(dequote(argValue));
230 return m;
233 private static boolean isHostPattern(final String s) {
234 return s.indexOf('*') >= 0 || s.indexOf('?') >= 0;
237 private static boolean isHostMatch(final String pattern, final String name) {
238 final FileNameMatcher fn;
239 try {
240 fn = new FileNameMatcher(pattern, null);
241 } catch (InvalidPatternException e) {
242 return false;
244 fn.append(name);
245 return fn.isMatch();
248 private static String dequote(final String value) {
249 if (value.startsWith("\"") && value.endsWith("\""))
250 return value.substring(1, value.length() - 2);
251 return value;
254 private static String nows(final String value) {
255 final StringBuilder b = new StringBuilder();
256 for (int i = 0; i < value.length(); i++) {
257 if (!Character.isSpaceChar(value.charAt(i)))
258 b.append(value.charAt(i));
260 return b.toString();
263 private File toFile(final String path) {
264 if (path.startsWith("~/"))
265 return new File(home, path.substring(2));
266 File ret = new File(path);
267 if (ret.isAbsolute())
268 return ret;
269 return new File(home, path);
273 * Configuration of one "Host" block in the configuration file.
274 * <p>
275 * If returned from {@link OpenSshConfig#lookup(String)} some or all of the
276 * properties may not be populated. The properties which are not populated
277 * should be defaulted by the caller.
278 * <p>
279 * When returned from {@link OpenSshConfig#lookup(String)} any wildcard
280 * entries which appear later in the configuration file will have been
281 * already merged into this block.
283 public static class Host {
284 boolean patternsApplied;
286 String hostName;
288 int port;
290 File identityFile;
292 String user;
294 String preferredAuthentications;
296 void copyFrom(final Host src) {
297 if (hostName == null)
298 hostName = src.hostName;
299 if (port == 0)
300 port = src.port;
301 if (identityFile == null)
302 identityFile = src.identityFile;
303 if (user == null)
304 user = src.user;
305 if (preferredAuthentications == null)
306 preferredAuthentications = src.preferredAuthentications;
310 * @return the real IP address or host name to connect to; never null.
312 public String getHostName() {
313 return hostName;
317 * @return the real port number to connect to; never 0.
319 public int getPort() {
320 return port;
324 * @return path of the private key file to use for authentication; null
325 * if the caller should use default authentication strategies.
327 public File getIdentityFile() {
328 return identityFile;
332 * @return the real user name to connect as; never null.
334 public String getUser() {
335 return user;
339 * @return the preferred authentication methods, separated by commas if
340 * more than one authentication method is preferred.
342 public String getPreferredAuthentications() {
343 return preferredAuthentications;