blob: 82dace3f79107a6982387d8c48b0f1f8cb033a29 [file] [log] [blame]
/*
* Copyright 2000-2012 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.git4idea.ssh;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
import java.util.regex.Pattern;
/**
* This class allows accessing information in OpenSSH configuration file.
*/
public class SSHConfig {
/**
* Entry list for config
*/
final List<HostEntry> myEntries = new ArrayList<HostEntry>();
/**
* User home directory
*/
@NonNls public final static String USER_HOME;
static {
String e = System.getenv("HOME");
USER_HOME = e != null ? e : System.getProperty("user.home");
}
/**
* Allowed authentication methods
*/
@NonNls private final static HashSet<String> ALLOWED_METHODS = new HashSet<String>();
static {
ALLOWED_METHODS.add(SSHMain.PUBLIC_KEY_METHOD);
ALLOWED_METHODS.add(SSHMain.KEYBOARD_INTERACTIVE_METHOD);
ALLOWED_METHODS.add(SSHMain.PASSWORD_METHOD);
}
/**
* Look the host up in the host database
*
* @param user a user name
* @param host a host name
* @param port a port
* @return a create host entry
*/
@NotNull
public Host lookup(@Nullable String user, @NotNull String host, @Nullable Integer port) {
final Host rc = new Host();
entriesLoop:
for (HostEntry e : myEntries) {
for (Pattern p : e.myNegative) {
if (p.matcher(host).matches()) {
continue entriesLoop;
}
}
if (e.myExactPositive.contains(host)) {
Host.merge(e.myHost, rc, rc);
continue;
}
for (Pattern p : e.myPositive) {
if (p.matcher(host).matches()) {
Host.merge(rc, e.myHost, rc);
}
}
}
// force user specified user and host names
if (user != null) {
rc.myUser = user;
}
if (port != null) {
rc.myPort = port;
}
if (rc.myHostName == null) {
rc.myHostName = host;
}
rc.setDefaults();
return rc;
}
/**
* @return a SSH user configuration file
* @throws IOException if config could not be loaded
*/
@SuppressWarnings({"HardCodedStringLiteral"})
@NotNull
public static SSHConfig load() throws IOException {
SSHConfig rc = new SSHConfig();
File configFile = new File(USER_HOME + File.separatorChar + ".ssh", "config");
if (!configFile.exists()) {
// no config file = empty config file
return rc;
}
BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(configFile), "ISO-8859-1"));
try {
Host host = null;
String line;
while ((line = in.readLine()) != null) {
line = line.trim();
if (line.length() == 0 || line.startsWith("#")) {
continue;
}
final String[] parts = line.split("[ \t]*[= \t]", 2);
final String keyword = parts[0];
final String argument = unquoteIfNeeded(parts[1]);
if ("Host".equalsIgnoreCase(keyword)) {
HostEntry entry = new HostEntry(argument);
rc.myEntries.add(entry);
host = entry.myHost;
continue;
}
if (host == null) {
HostEntry entry = new HostEntry("*");
rc.myEntries.add(entry);
host = entry.myHost;
}
if ("BatchMode".equalsIgnoreCase(keyword)) {
host.myBatchMode = parseBoolean(argument);
}
else if ("HostKeyAlgorithms".equalsIgnoreCase(keyword)) {
host.myHostKeyAlgorithms = Collections.unmodifiableList(parseList(argument));
}
else if ("HostKeyAlias".equalsIgnoreCase(keyword)) {
host.myHostKeyAlias = argument;
}
else if ("HostName".equalsIgnoreCase(keyword)) {
host.myHostName = argument;
}
else if ("IdentityFile".equalsIgnoreCase(keyword)) {
host.myIdentityFile = argument;
}
else if ("NumberOfPasswordPrompts".equalsIgnoreCase(keyword)) {
host.myNumberOfPasswordPrompts = parseInt(argument);
}
else if ("PasswordAuthentication".equalsIgnoreCase(keyword)) {
host.myPasswordAuthentication = parseBoolean(argument);
}
else if ("Port".equalsIgnoreCase(keyword)) {
host.myPort = parseInt(argument);
}
else if ("PreferredAuthentications".equalsIgnoreCase(keyword)) {
final List<String> list = parseList(argument);
list.retainAll(ALLOWED_METHODS);
if (!list.isEmpty()) {
host.myPreferredMethods = Collections.unmodifiableList(list);
}
}
else if ("PubkeyAuthentication".equalsIgnoreCase(keyword)) {
host.myPubkeyAuthentication = parseBoolean(argument);
}
else if ("User".equalsIgnoreCase(keyword)) {
host.myUser = argument;
}
}
}
finally {
in.close();
}
return rc;
}
/**
* Parse integer and return null if integer is incorrect
*
* @param value an argument to parse
* @return {@link Integer} or null.
*/
private static Integer parseInt(final String value) {
try {
return Integer.parseInt(value);
}
catch (NumberFormatException e) {
return null;
}
}
/**
* Parse file name handling %d %u %l %h %r options
*
* @param host the host entry
* @param value a file name to parse
* @return actual file name
*/
private static File parseFileName(Host host, final String value) {
try {
StringBuilder rc = new StringBuilder();
for (int i = 0; i < value.length(); i++) {
char ch = value.charAt(i);
if (i == 0 && ch == '~') {
rc.append(USER_HOME);
continue;
}
if (ch == '%' && i + 1 < value.length()) {
//noinspection AssignmentToForLoopParameter
i++;
switch (value.charAt(i)) {
case '%':
rc.append('%');
break;
case 'd':
rc.append(USER_HOME);
break;
case 'h':
rc.append(host.getHostName());
break;
case 'l':
rc.append(InetAddress.getLocalHost().getHostName());
break;
case 'r':
rc.append(host.getUser());
break;
default:
rc.append('%');
rc.append(ch);
break;
}
}
rc.append(ch);
}
return new File(rc.toString());
}
catch (UnknownHostException e) {
return null;
}
}
/**
* Parse boolean value
*
* @param value an value to parse
* @return a boolean value
*/
private static Boolean parseBoolean(final String value) {
//noinspection HardCodedStringLiteral
return "yes".equals(value);
}
private static List<String> parseList(final String arg) {
List<String> values = new ArrayList<String>();
for (String a : arg.split("[ \t,]+")) {
if (a.length() == 0) {
continue;
}
values.add(a);
}
return values;
}
/**
* Unquote string if needed
*
* @param part a string to unquote
* @return unquoted value
*/
private static String unquoteIfNeeded(String part) {
if (part.length() > 1 && part.charAt(0) == '"' && part.charAt(part.length() - 1) == '"') {
part = part.substring(1, part.length() - 1);
}
return part.trim();
}
/**
* Host entry in the file
*/
private static class HostEntry {
/**
* Negative patterns
*/
private final List<Pattern> myNegative = new ArrayList<Pattern>();
/**
* Positive patterns
*/
private final List<Pattern> myPositive = new ArrayList<Pattern>();
/**
* Exact positive patterns that match host exactly rather than by mask
*/
private final List<String> myExactPositive = new ArrayList<String>();
/**
* The host entry
*/
private final Host myHost = new Host();
/**
* Create host entry from patterns
*
* @param patterns a patterns to match
*/
public HostEntry(final String patterns) {
for (String pattern : patterns.split("[\t ,]+")) {
if (pattern.length() == 0) {
continue;
}
if (pattern.startsWith("!")) {
myNegative.add(compilePattern(pattern.substring(1).trim()));
}
else if (pattern.indexOf('?') == 0 && pattern.indexOf('*') == 0) {
myExactPositive.add(pattern);
}
else {
myPositive.add(compilePattern(pattern));
}
}
}
/**
* Convert host pattern from glob format to regexp. Note that the function assumes
* a valid host pattern, so characters that might not happen in the host name but
* must be escaped in regular expressions are not escaped.
*
* @param s a pattern to convert
* @return the resulting pattern
*/
private static Pattern compilePattern(final String s) {
StringBuilder rc = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
switch (ch) {
case '?':
rc.append('.');
break;
case '*':
rc.append(".*");
break;
case '.':
// for non-host strings more characters
rc.append('\\');
default:
rc.append(ch);
}
}
return Pattern.compile(rc.toString());
}
}
/**
* Host information
*/
public static class Host {
/**
* User name
*/
@Nullable private String myUser;
/**
* The name of the host
*/
@Nullable private String myHostName;
/**
* The port number
*/
@Nullable private Integer myPort;
/**
* Identity file
*/
@Nullable private String myIdentityFile;
/**
* Preferred authentication methods
*/
@Nullable private List<String> myPreferredMethods;
/**
* Preferred authentication methods
*/
@Nullable private List<String> myHostKeyAlgorithms;
/**
* Batch mode parameter
*/
@Nullable private Boolean myBatchMode;
/**
* Alias for host key
*/
@Nullable private String myHostKeyAlias;
/**
* Number of password prompts
*/
@Nullable private Integer myNumberOfPasswordPrompts;
/**
* If true password authentication is enabled
*/
@Nullable private Boolean myPasswordAuthentication;
/**
* If true, public key authentication is allowed
*/
@Nullable private Boolean myPubkeyAuthentication;
/**
* @return remote user name
*/
@NotNull
public String getUser() {
return notNull(myUser);
}
/**
* @return real host name
*/
@NotNull
public String getHostName() {
return notNull(myHostName);
}
/**
* @return port number
*/
@SuppressWarnings({"NullableProblems"})
public int getPort() {
return notNull(myPort).intValue();
}
/**
* @return identity file or null if the default for algorithm should be used
*/
@Nullable
public File getIdentityFile() {
if (myIdentityFile == null) {
return null;
}
return parseFileName(this, myIdentityFile);
}
/**
* @return preferred authentication methods
*/
@NotNull
public List<String> getPreferredMethods() {
return notNull(myPreferredMethods);
}
/**
* @return true if the host should be use in the batch mode
*/
@SuppressWarnings({"NullableProblems"})
public boolean isBatchMode() {
return notNull(myBatchMode).booleanValue();
}
/**
* @return algorithms that should be used for the host
*/
@NotNull
public List<String> getHostKeyAlgorithms() {
return notNull(myHostKeyAlgorithms);
}
/**
* @return the number of the password prompts
*/
@SuppressWarnings({"NullableProblems"})
public int getNumberOfPasswordPrompts() {
return notNull(myNumberOfPasswordPrompts).intValue();
}
/**
* @return alias for host to be used in known hosts file
*/
@NotNull
public String getHostKeyAlias() {
return notNull(myHostKeyAlias);
}
/**
* @return true if password authentication is supported for the host
*/
public boolean supportsPasswordAuthentication() {
return notNull(myPasswordAuthentication).booleanValue();
}
/**
* @return true if public key authentication is supported for the host
*/
public boolean supportsPubkeyAuthentication() {
return notNull(myPubkeyAuthentication).booleanValue();
}
/**
* Set defaults for unspecified fields
*/
@SuppressWarnings({"HardCodedStringLiteral"})
private void setDefaults() {
if (myUser == null) {
myUser = System.getProperty("user.name");
}
if (myPort == null) {
myPort = 22;
}
if (myPreferredMethods == null) {
myPreferredMethods = Collections
.unmodifiableList(Arrays.asList(SSHMain.PUBLIC_KEY_METHOD, SSHMain.KEYBOARD_INTERACTIVE_METHOD, SSHMain.PASSWORD_METHOD));
}
if (myBatchMode == null) {
myBatchMode = Boolean.FALSE;
}
if (myHostKeyAlgorithms == null) {
myHostKeyAlgorithms = Collections.unmodifiableList(Arrays.asList(SSHMain.SSH_RSA_ALGORITHM, SSHMain.SSH_DSS_ALGORITHM));
}
if (myNumberOfPasswordPrompts == null) {
myNumberOfPasswordPrompts = 3;
}
if (myHostKeyAlias == null) {
myHostKeyAlias = myHostName;
}
if (myPasswordAuthentication == null) {
myPasswordAuthentication = Boolean.TRUE;
}
if (myPubkeyAuthentication == null) {
myPubkeyAuthentication = Boolean.TRUE;
}
}
/**
* Ensure that the value is not null
*
* @param value the value to check
* @param <T> the parameter type
* @return the provided argument if not null
* @throws IllegalStateException if the value is null
*/
@NotNull
private static <T> T notNull(@Nullable T value) {
if (value == null) {
throw new IllegalStateException("The value must not be null");
}
return value;
}
/**
* Merge information from two host entries
*
* @param first this entry is the preferred
* @param second this entry provides information only if {@code first) did not have one
* @param result this entry is updated as result of merge (usually {@code first) or {@code second))
*/
private static void merge(Host first, Host second, Host result) {
result.myUser = mergeValue(first.myUser, second.myUser);
result.myHostName = mergeValue(first.myHostName, second.myHostName);
result.myPort = mergeValue(first.myPort, second.myPort);
result.myIdentityFile = mergeValue(first.myIdentityFile, second.myIdentityFile);
result.myPreferredMethods = mergeValue(first.myPreferredMethods, second.myPreferredMethods);
result.myHostKeyAlgorithms = mergeValue(first.myHostKeyAlgorithms, second.myHostKeyAlgorithms);
result.myBatchMode = mergeValue(first.myBatchMode, second.myBatchMode);
result.myHostKeyAlias = mergeValue(first.myHostKeyAlias, second.myHostKeyAlias);
result.myNumberOfPasswordPrompts = mergeValue(first.myNumberOfPasswordPrompts, second.myNumberOfPasswordPrompts);
result.myPasswordAuthentication = mergeValue(first.myPasswordAuthentication, second.myPasswordAuthentication);
result.myPubkeyAuthentication = mergeValue(first.myPubkeyAuthentication, second.myPubkeyAuthentication);
}
/**
* Merge single value
*
* @param first the first value
* @param second the second value
* @param <T> a value type
* @return a result of merge
*/
private static <T> T mergeValue(T first, T second) {
return first == null ? second : first;
}
@Override
public String toString() {
return String.format("Host{myUser='%s', myHostName='%s', myPort=%d, myIdentityFile='%s'}",
myUser, myHostName, myPort, myIdentityFile);
}
}
}