2 * Copyright (C) 2008, Google Inc.
6 * Redistribution and use in source and binary forms, with or
7 * without modification, are permitted provided that the following
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
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
;
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
;
53 import org
.spearce
.jgit
.errors
.InvalidPatternException
;
54 import org
.spearce
.jgit
.fnmatch
.FileNameMatcher
;
55 import org
.spearce
.jgit
.util
.FS
;
58 * Simple configuration parser for the OpenSSH ~/.ssh/config file.
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
{
66 * Obtain the user's configuration data.
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();
78 home
= new File(".").getAbsoluteFile();
80 final File config
= new File(new File(home
, ".ssh"), "config");
81 final OpenSshConfig osc
= new OpenSshConfig(home
, config
);
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
) {
101 hosts
= Collections
.emptyMap();
105 * Locate the configuration for a specific host request.
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
);
118 if (h
.patternsApplied
)
121 for (final Map
.Entry
<String
, Host
> e
: cache
.entrySet()) {
122 if (!isHostPattern(e
.getKey()))
124 if (!isHostMatch(e
.getKey(), hostName
))
126 h
.copyFrom(e
.getValue());
129 if (h
.hostName
== null)
130 h
.hostName
= hostName
;
132 h
.user
= DefaultSshSessionFactory
.userName();
134 h
.port
= DefaultSshSessionFactory
.SSH_PORT
;
135 h
.patternsApplied
= true;
139 private synchronized Map
<String
, Host
> refresh() {
140 final long mtime
= configFile
.lastModified();
141 if (mtime
!= lastModified
) {
143 final FileInputStream in
= new FileInputStream(configFile
);
149 } catch (FileNotFoundException none
) {
150 hosts
= Collections
.emptyMap();
151 } catch (IOException err
) {
152 hosts
= Collections
.emptyMap();
154 lastModified
= mtime
;
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);
165 while ((line
= br
.readLine()) != null) {
167 if (line
.length() == 0 || line
.startsWith("#"))
170 final int sp
= line
.indexOf(' ');
171 final int eq
= line
.indexOf('=');
173 if (sp
>= 0 && eq
>= 0)
174 splitAt
= Math
.min(sp
, eq
);
179 final String keyword
= line
.substring(0, splitAt
).trim();
180 final String argValue
= line
.substring(splitAt
+ 1).trim();
182 if ("Host".equalsIgnoreCase(keyword
)) {
184 for (final String name
: argValue
.split("[ \t]")) {
185 Host c
= m
.get(name
);
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.
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
)
209 c
.user
= dequote(argValue
);
210 } else if ("Port".equalsIgnoreCase(keyword
)) {
212 final int port
= Integer
.parseInt(dequote(argValue
));
213 for (final Host c
: current
)
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
));
229 private static boolean isHostPattern(final String s
) {
230 return s
.indexOf('*') >= 0 || s
.indexOf('?') >= 0;
233 private static boolean isHostMatch(final String pattern
, final String name
) {
234 final FileNameMatcher fn
;
236 fn
= new FileNameMatcher(pattern
, null);
237 } catch (InvalidPatternException e
) {
244 private static String
dequote(final String value
) {
245 if (value
.startsWith("\"") && value
.endsWith("\""))
246 return value
.substring(1, value
.length() - 2);
250 private File
toFile(final String path
) {
251 if (path
.startsWith("~/"))
252 return new File(home
, path
.substring(2));
253 File ret
= new File(path
);
254 if (ret
.isAbsolute())
256 return new File(home
, path
);
260 * Configuration of one "Host" block in the configuration file.
262 * If returned from {@link OpenSshConfig#lookup(String)} some or all of the
263 * properties may not be populated. The properties which are not populated
264 * should be defaulted by the caller.
266 * When returned from {@link OpenSshConfig#lookup(String)} any wildcard
267 * entries which appear later in the configuration file will have been
268 * already merged into this block.
270 public static class Host
{
271 boolean patternsApplied
;
281 void copyFrom(final Host src
) {
282 if (hostName
== null)
283 hostName
= src
.hostName
;
286 if (identityFile
== null)
287 identityFile
= src
.identityFile
;
293 * @return the real IP address or host name to connect to; never null.
295 public String
getHostName() {
300 * @return the real port number to connect to; never 0.
302 public int getPort() {
307 * @return path of the private key file to use for authentication; null
308 * if the caller should use default authentication strategies.
310 public File
getIdentityFile() {
315 * @return the real user name to connect as; never null.
317 public String
getUser() {