| /* |
| * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> |
| * and other copyright owners as documented in the project's IP log. |
| * |
| * This program and the accompanying materials are made available |
| * under the terms of the Eclipse Distribution License v1.0 which |
| * accompanies this distribution, is reproduced below, and is |
| * available at http://www.eclipse.org/org/documents/edl-v10.php |
| * |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or |
| * without modification, are permitted provided that the following |
| * conditions are met: |
| * |
| * - Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * |
| * - Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following |
| * disclaimer in the documentation and/or other materials provided |
| * with the distribution. |
| * |
| * - Neither the name of the Eclipse Foundation, Inc. nor the |
| * names of its contributors may be used to endorse or promote |
| * products derived from this software without specific prior |
| * written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
| * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
| * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
| * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
| * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
| * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
| * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| package org.eclipse.jgit.internal.transport.sshd; |
| |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| import static java.text.MessageFormat.format; |
| |
| import java.io.BufferedReader; |
| import java.io.BufferedWriter; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.OutputStreamWriter; |
| import java.net.InetSocketAddress; |
| import java.net.SocketAddress; |
| import java.nio.file.Files; |
| import java.nio.file.InvalidPathException; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.security.GeneralSecurityException; |
| import java.security.PublicKey; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.function.Supplier; |
| |
| import org.apache.sshd.client.config.hosts.HostConfigEntry; |
| import org.apache.sshd.client.config.hosts.KnownHostEntry; |
| import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier; |
| import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair; |
| import org.apache.sshd.client.keyverifier.ServerKeyVerifier; |
| import org.apache.sshd.client.session.ClientSession; |
| import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; |
| import org.apache.sshd.common.config.keys.KeyUtils; |
| import org.apache.sshd.common.config.keys.PublicKeyEntryResolver; |
| import org.apache.sshd.common.digest.BuiltinDigests; |
| import org.apache.sshd.common.util.io.ModifiableFileWatcher; |
| import org.apache.sshd.common.util.net.SshdSocketAddress; |
| import org.eclipse.jgit.internal.storage.file.LockFile; |
| import org.eclipse.jgit.transport.CredentialItem; |
| import org.eclipse.jgit.transport.CredentialsProvider; |
| import org.eclipse.jgit.transport.SshConstants; |
| import org.eclipse.jgit.transport.URIish; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * A sever host key verifier that honors the {@code StrictHostKeyChecking} and |
| * {@code UserKnownHostsFile} values from the ssh configuration. |
| * <p> |
| * The verifier can be given default known_hosts files in the constructor, which |
| * will be used if the ssh config does not specify a {@code UserKnownHostsFile}. |
| * If the ssh config <em>does</em> set {@code UserKnownHostsFile}, the verifier |
| * uses the given files in the order given. Non-existing or unreadable files are |
| * ignored. |
| * <p> |
| * {@code StrictHostKeyChecking} accepts the following values: |
| * </p> |
| * <dl> |
| * <dt>ask</dt> |
| * <dd>Ask the user whether new or changed keys shall be accepted and be added |
| * to the known_hosts file.</dd> |
| * <dt>yes/true</dt> |
| * <dd>Accept only keys listed in the known_hosts file.</dd> |
| * <dt>no/false</dt> |
| * <dd>Silently accept all new or changed keys, add new keys to the known_hosts |
| * file.</dd> |
| * <dt>accept-new</dt> |
| * <dd>Silently accept keys for new hosts and add them to the known_hosts |
| * file.</dd> |
| * </dl> |
| * <p> |
| * If {@code StrictHostKeyChecking} is not set, or set to any other value, the |
| * default value <b>ask</b> is active. |
| * </p> |
| * <p> |
| * This implementation relies on the {@link ClientSession} being a |
| * {@link JGitClientSession}. By default Apache MINA sshd does not forward the |
| * config file host entry to the session, so it would be unknown here which |
| * entry it was and what setting of {@code StrictHostKeyChecking} should be |
| * used. If used with some other session type, the implementation assumes |
| * "<b>ask</b>". |
| * <p> |
| * <p> |
| * Asking the user is done via a {@link CredentialsProvider} obtained from the |
| * session. If none is set, the implementation falls back to strict host key |
| * checking ("<b>yes</b>"). |
| * </p> |
| * <p> |
| * Note that adding a key to the known hosts file may create the file. You can |
| * specify in the constructor whether the user shall be asked about that, too. |
| * If the user declines updating the file, but the key was otherwise |
| * accepted (user confirmed for "<b>ask</b>", or "no" or "accept-new" are |
| * active), the key is accepted for this session only. |
| * </p> |
| * <p> |
| * If several known hosts files are specified, a new key is always added to the |
| * first file (even if it doesn't exist yet; see the note about file creation |
| * above). |
| * </p> |
| * |
| * @see <a href="http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5">man |
| * ssh-config</a> |
| */ |
| public class OpenSshServerKeyVerifier |
| implements ServerKeyVerifier, ServerKeyLookup { |
| |
| // TODO: GlobalKnownHostsFile? May need some kind of LRU caching; these |
| // files may be large! |
| |
| private static final Logger LOG = LoggerFactory |
| .getLogger(OpenSshServerKeyVerifier.class); |
| |
| /** Can be used to mark revoked known host lines. */ |
| private static final String MARKER_REVOKED = "revoked"; //$NON-NLS-1$ |
| |
| private final boolean askAboutNewFile; |
| |
| private final Map<Path, HostKeyFile> knownHostsFiles = new ConcurrentHashMap<>(); |
| |
| private final List<HostKeyFile> defaultFiles = new ArrayList<>(); |
| |
| private enum ModifiedKeyHandling { |
| DENY, ALLOW, ALLOW_AND_STORE |
| } |
| |
| /** |
| * Creates a new {@link OpenSshServerKeyVerifier}. |
| * |
| * @param askAboutNewFile |
| * whether to ask the user, if possible, about creating a new |
| * non-existing known_hosts file |
| * @param defaultFiles |
| * typically ~/.ssh/known_hosts and ~/.ssh/known_hosts2. May be |
| * empty or {@code null}, in which case no default files are |
| * installed. The files need not exist. |
| */ |
| public OpenSshServerKeyVerifier(boolean askAboutNewFile, |
| List<Path> defaultFiles) { |
| if (defaultFiles != null) { |
| for (Path file : defaultFiles) { |
| HostKeyFile newFile = new HostKeyFile(file); |
| knownHostsFiles.put(file, newFile); |
| this.defaultFiles.add(newFile); |
| } |
| } |
| this.askAboutNewFile = askAboutNewFile; |
| } |
| |
| private List<HostKeyFile> getFilesToUse(ClientSession session) { |
| List<HostKeyFile> filesToUse = defaultFiles; |
| if (session instanceof JGitClientSession) { |
| HostConfigEntry entry = ((JGitClientSession) session) |
| .getHostConfigEntry(); |
| if (entry instanceof JGitHostConfigEntry) { |
| // Always true! |
| List<HostKeyFile> userFiles = addUserHostKeyFiles( |
| ((JGitHostConfigEntry) entry).getMultiValuedOptions() |
| .get(SshConstants.USER_KNOWN_HOSTS_FILE)); |
| if (!userFiles.isEmpty()) { |
| filesToUse = userFiles; |
| } |
| } |
| } |
| return filesToUse; |
| } |
| |
| @Override |
| public List<HostEntryPair> lookup(ClientSession session, |
| SocketAddress remote) { |
| List<HostKeyFile> filesToUse = getFilesToUse(session); |
| HostKeyHelper helper = new HostKeyHelper(); |
| List<HostEntryPair> result = new ArrayList<>(); |
| Collection<SshdSocketAddress> candidates = helper |
| .resolveHostNetworkIdentities(session, remote); |
| for (HostKeyFile file : filesToUse) { |
| for (HostEntryPair current : file.get()) { |
| KnownHostEntry entry = current.getHostEntry(); |
| for (SshdSocketAddress host : candidates) { |
| if (entry.isHostMatch(host.getHostName(), host.getPort())) { |
| result.add(current); |
| break; |
| } |
| } |
| } |
| } |
| return result; |
| } |
| |
| @Override |
| public boolean verifyServerKey(ClientSession clientSession, |
| SocketAddress remoteAddress, PublicKey serverKey) { |
| List<HostKeyFile> filesToUse = getFilesToUse(clientSession); |
| AskUser ask = new AskUser(); |
| HostEntryPair[] modified = { null }; |
| Path path = null; |
| HostKeyHelper helper = new HostKeyHelper(); |
| for (HostKeyFile file : filesToUse) { |
| try { |
| if (find(clientSession, remoteAddress, serverKey, file.get(), |
| modified, helper)) { |
| return true; |
| } |
| } catch (RevokedKeyException e) { |
| ask.revokedKey(clientSession, remoteAddress, serverKey, |
| file.getPath()); |
| return false; |
| } |
| if (path == null && modified[0] != null) { |
| // Remember the file in which we might need to update the |
| // entry |
| path = file.getPath(); |
| } |
| } |
| if (modified[0] != null) { |
| // We found an entry, but with a different key |
| ModifiedKeyHandling toDo = ask.acceptModifiedServerKey( |
| clientSession, remoteAddress, modified[0].getServerKey(), |
| serverKey, path); |
| if (toDo == ModifiedKeyHandling.ALLOW_AND_STORE) { |
| try { |
| updateModifiedServerKey(clientSession, remoteAddress, |
| serverKey, modified[0], path, helper); |
| knownHostsFiles.get(path).resetReloadAttributes(); |
| } catch (IOException e) { |
| LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate, |
| path)); |
| } |
| } |
| if (toDo == ModifiedKeyHandling.DENY) { |
| return false; |
| } |
| // TODO: OpenSsh disables password and keyboard-interactive |
| // authentication in this case. Also agent and local port forwarding |
| // are switched off. (Plus a few other things such as X11 forwarding |
| // that are of no interest to a git client.) |
| return true; |
| } else if (ask.acceptUnknownKey(clientSession, remoteAddress, |
| serverKey)) { |
| if (!filesToUse.isEmpty()) { |
| HostKeyFile toUpdate = filesToUse.get(0); |
| path = toUpdate.getPath(); |
| try { |
| updateKnownHostsFile(clientSession, remoteAddress, |
| serverKey, path, helper); |
| toUpdate.resetReloadAttributes(); |
| } catch (IOException e) { |
| LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate, |
| path)); |
| } |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| private static class RevokedKeyException extends Exception { |
| private static final long serialVersionUID = 1L; |
| } |
| |
| private boolean find(ClientSession clientSession, |
| SocketAddress remoteAddress, PublicKey serverKey, |
| List<HostEntryPair> entries, HostEntryPair[] modified, |
| HostKeyHelper helper) throws RevokedKeyException { |
| Collection<SshdSocketAddress> candidates = helper |
| .resolveHostNetworkIdentities(clientSession, remoteAddress); |
| for (HostEntryPair current : entries) { |
| KnownHostEntry entry = current.getHostEntry(); |
| for (SshdSocketAddress host : candidates) { |
| if (entry.isHostMatch(host.getHostName(), host.getPort())) { |
| boolean isRevoked = MARKER_REVOKED |
| .equals(entry.getMarker()); |
| if (KeyUtils.compareKeys(serverKey, |
| current.getServerKey())) { |
| // Exact match |
| if (isRevoked) { |
| throw new RevokedKeyException(); |
| } |
| modified[0] = null; |
| return true; |
| } else if (!isRevoked) { |
| // Server sent a different key |
| modified[0] = current; |
| // Keep going -- maybe there's another entry for this |
| // host |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| private List<HostKeyFile> addUserHostKeyFiles(List<String> fileNames) { |
| if (fileNames == null || fileNames.isEmpty()) { |
| return Collections.emptyList(); |
| } |
| List<HostKeyFile> userFiles = new ArrayList<>(); |
| for (String name : fileNames) { |
| try { |
| Path path = Paths.get(name); |
| HostKeyFile file = knownHostsFiles.computeIfAbsent(path, |
| p -> new HostKeyFile(path)); |
| userFiles.add(file); |
| } catch (InvalidPathException e) { |
| LOG.warn(format(SshdText.get().knownHostsInvalidPath, |
| name)); |
| } |
| } |
| return userFiles; |
| } |
| |
| private void updateKnownHostsFile(ClientSession clientSession, |
| SocketAddress remoteAddress, PublicKey serverKey, Path path, |
| HostKeyHelper updater) |
| throws IOException { |
| KnownHostEntry entry = updater.prepareKnownHostEntry(clientSession, |
| remoteAddress, serverKey); |
| if (entry == null) { |
| return; |
| } |
| if (!Files.exists(path)) { |
| if (askAboutNewFile) { |
| CredentialsProvider provider = getCredentialsProvider( |
| clientSession); |
| if (provider == null) { |
| // We can't ask, so don't create the file |
| return; |
| } |
| URIish uri = new URIish().setPath(path.toString()); |
| if (!askUser(provider, uri, // |
| format(SshdText.get().knownHostsUserAskCreationPrompt, |
| path), // |
| format(SshdText.get().knownHostsUserAskCreationMsg, |
| path))) { |
| return; |
| } |
| } |
| } |
| LockFile lock = new LockFile(path.toFile()); |
| if (lock.lockForAppend()) { |
| try { |
| try (BufferedWriter writer = new BufferedWriter( |
| new OutputStreamWriter(lock.getOutputStream(), |
| UTF_8))) { |
| writer.newLine(); |
| writer.write(entry.getConfigLine()); |
| writer.newLine(); |
| } |
| lock.commit(); |
| } catch (IOException e) { |
| lock.unlock(); |
| throw e; |
| } |
| } else { |
| LOG.warn(format(SshdText.get().knownHostsFileLockedUpdate, |
| path)); |
| } |
| } |
| |
| private void updateModifiedServerKey(ClientSession clientSession, |
| SocketAddress remoteAddress, PublicKey serverKey, |
| HostEntryPair entry, Path path, HostKeyHelper helper) |
| throws IOException { |
| KnownHostEntry hostEntry = entry.getHostEntry(); |
| String oldLine = hostEntry.getConfigLine(); |
| String newLine = helper.prepareModifiedServerKeyLine(clientSession, |
| remoteAddress, hostEntry, oldLine, entry.getServerKey(), |
| serverKey); |
| if (newLine == null || newLine.isEmpty()) { |
| return; |
| } |
| if (oldLine == null || oldLine.isEmpty() || newLine.equals(oldLine)) { |
| // Shouldn't happen. |
| return; |
| } |
| LockFile lock = new LockFile(path.toFile()); |
| if (lock.lock()) { |
| try { |
| try (BufferedWriter writer = new BufferedWriter( |
| new OutputStreamWriter(lock.getOutputStream(), UTF_8)); |
| BufferedReader reader = Files.newBufferedReader(path, |
| UTF_8)) { |
| boolean done = false; |
| String line; |
| while ((line = reader.readLine()) != null) { |
| String toWrite = line; |
| if (!done) { |
| int pos = line.indexOf('#'); |
| String toTest = pos < 0 ? line |
| : line.substring(0, pos); |
| if (toTest.trim().equals(oldLine)) { |
| toWrite = newLine; |
| done = true; |
| } |
| } |
| writer.write(toWrite); |
| writer.newLine(); |
| } |
| } |
| lock.commit(); |
| } catch (IOException e) { |
| lock.unlock(); |
| throw e; |
| } |
| } else { |
| LOG.warn(format(SshdText.get().knownHostsFileLockedUpdate, |
| path)); |
| } |
| } |
| |
| private static CredentialsProvider getCredentialsProvider( |
| ClientSession session) { |
| if (session instanceof JGitClientSession) { |
| return ((JGitClientSession) session).getCredentialsProvider(); |
| } |
| return null; |
| } |
| |
| private static boolean askUser(CredentialsProvider provider, URIish uri, |
| String prompt, String... messages) { |
| List<CredentialItem> items = new ArrayList<>(messages.length + 1); |
| for (String message : messages) { |
| items.add(new CredentialItem.InformationalMessage(message)); |
| } |
| if (prompt != null) { |
| CredentialItem.YesNoType answer = new CredentialItem.YesNoType( |
| prompt); |
| items.add(answer); |
| return provider.get(uri, items) && answer.getValue(); |
| } else { |
| return provider.get(uri, items); |
| } |
| } |
| |
| private static class AskUser { |
| |
| private enum Check { |
| ASK, DENY, ALLOW; |
| } |
| |
| @SuppressWarnings("nls") |
| private Check checkMode(ClientSession session, |
| SocketAddress remoteAddress, boolean changed) { |
| if (!(remoteAddress instanceof InetSocketAddress)) { |
| return Check.DENY; |
| } |
| if (session instanceof JGitClientSession) { |
| HostConfigEntry entry = ((JGitClientSession) session) |
| .getHostConfigEntry(); |
| String value = entry.getProperty( |
| SshConstants.STRICT_HOST_KEY_CHECKING, "ask"); |
| switch (value.toLowerCase(Locale.ROOT)) { |
| case SshConstants.YES: |
| case SshConstants.ON: |
| return Check.DENY; |
| case SshConstants.NO: |
| case SshConstants.OFF: |
| return Check.ALLOW; |
| case "accept-new": |
| return changed ? Check.DENY : Check.ALLOW; |
| default: |
| break; |
| } |
| } |
| if (getCredentialsProvider(session) == null) { |
| // This is called only for new, unknown hosts. If we have no way |
| // to interact with the user, the fallback mode is to deny the |
| // key. |
| return Check.DENY; |
| } |
| return Check.ASK; |
| } |
| |
| public void revokedKey(ClientSession clientSession, |
| SocketAddress remoteAddress, PublicKey serverKey, Path path) { |
| CredentialsProvider provider = getCredentialsProvider( |
| clientSession); |
| if (provider == null) { |
| return; |
| } |
| InetSocketAddress remote = (InetSocketAddress) remoteAddress; |
| URIish uri = JGitUserInteraction.toURI(clientSession.getUsername(), |
| remote); |
| String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256, |
| serverKey); |
| String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey); |
| String keyAlgorithm = serverKey.getAlgorithm(); |
| askUser(provider, uri, null, // |
| format(SshdText.get().knownHostsRevokedKeyMsg, |
| remote.getHostString(), path), |
| format(SshdText.get().knownHostsKeyFingerprints, |
| keyAlgorithm), |
| md5, sha256); |
| } |
| |
| public boolean acceptUnknownKey(ClientSession clientSession, |
| SocketAddress remoteAddress, PublicKey serverKey) { |
| Check check = checkMode(clientSession, remoteAddress, false); |
| if (check != Check.ASK) { |
| return check == Check.ALLOW; |
| } |
| CredentialsProvider provider = getCredentialsProvider( |
| clientSession); |
| InetSocketAddress remote = (InetSocketAddress) remoteAddress; |
| // Ask the user |
| String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256, |
| serverKey); |
| String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey); |
| String keyAlgorithm = serverKey.getAlgorithm(); |
| String remoteHost = remote.getHostString(); |
| URIish uri = JGitUserInteraction.toURI(clientSession.getUsername(), |
| remote); |
| String prompt = SshdText.get().knownHostsUnknownKeyPrompt; |
| return askUser(provider, uri, prompt, // |
| format(SshdText.get().knownHostsUnknownKeyMsg, |
| remoteHost), |
| format(SshdText.get().knownHostsKeyFingerprints, |
| keyAlgorithm), |
| md5, sha256); |
| } |
| |
| public ModifiedKeyHandling acceptModifiedServerKey( |
| ClientSession clientSession, |
| SocketAddress remoteAddress, PublicKey expected, |
| PublicKey actual, Path path) { |
| Check check = checkMode(clientSession, remoteAddress, true); |
| if (check == Check.ALLOW) { |
| // Never auto-store on CHECK.ALLOW |
| return ModifiedKeyHandling.ALLOW; |
| } |
| InetSocketAddress remote = (InetSocketAddress) remoteAddress; |
| String keyAlgorithm = actual.getAlgorithm(); |
| String remoteHost = remote.getHostString(); |
| URIish uri = JGitUserInteraction.toURI(clientSession.getUsername(), |
| remote); |
| List<String> messages = new ArrayList<>(); |
| String warning = format( |
| SshdText.get().knownHostsModifiedKeyWarning, |
| keyAlgorithm, expected.getAlgorithm(), remoteHost, |
| KeyUtils.getFingerPrint(BuiltinDigests.md5, expected), |
| KeyUtils.getFingerPrint(BuiltinDigests.sha256, expected), |
| KeyUtils.getFingerPrint(BuiltinDigests.md5, actual), |
| KeyUtils.getFingerPrint(BuiltinDigests.sha256, actual)); |
| messages.addAll(Arrays.asList(warning.split("\n"))); //$NON-NLS-1$ |
| |
| CredentialsProvider provider = getCredentialsProvider( |
| clientSession); |
| if (check == Check.DENY) { |
| if (provider != null) { |
| messages.add(format( |
| SshdText.get().knownHostsModifiedKeyDenyMsg, path)); |
| askUser(provider, uri, null, |
| messages.toArray(new String[0])); |
| } |
| return ModifiedKeyHandling.DENY; |
| } |
| // ASK -- two questions: procceed? and store? |
| List<CredentialItem> items = new ArrayList<>(messages.size() + 2); |
| for (String message : messages) { |
| items.add(new CredentialItem.InformationalMessage(message)); |
| } |
| CredentialItem.YesNoType proceed = new CredentialItem.YesNoType( |
| SshdText.get().knownHostsModifiedKeyAcceptPrompt); |
| CredentialItem.YesNoType store = new CredentialItem.YesNoType( |
| SshdText.get().knownHostsModifiedKeyStorePrompt); |
| items.add(proceed); |
| items.add(store); |
| if (provider.get(uri, items) && proceed.getValue()) { |
| return store.getValue() ? ModifiedKeyHandling.ALLOW_AND_STORE |
| : ModifiedKeyHandling.ALLOW; |
| } |
| return ModifiedKeyHandling.DENY; |
| } |
| |
| } |
| |
| private static class HostKeyFile extends ModifiableFileWatcher |
| implements Supplier<List<HostEntryPair>> { |
| |
| private List<HostEntryPair> entries = Collections.emptyList(); |
| |
| public HostKeyFile(Path path) { |
| super(path); |
| } |
| |
| @Override |
| public List<HostEntryPair> get() { |
| Path path = getPath(); |
| try { |
| if (checkReloadRequired()) { |
| if (!Files.exists(path)) { |
| // Has disappeared. |
| resetReloadAttributes(); |
| return Collections.emptyList(); |
| } |
| LockFile lock = new LockFile(path.toFile()); |
| if (lock.lock()) { |
| try { |
| entries = reload(getPath()); |
| } finally { |
| lock.unlock(); |
| } |
| } else { |
| LOG.warn(format(SshdText.get().knownHostsFileLockedRead, |
| path)); |
| } |
| } |
| } catch (IOException e) { |
| LOG.warn(format(SshdText.get().knownHostsFileReadFailed, path)); |
| } |
| return Collections.unmodifiableList(entries); |
| } |
| |
| private List<HostEntryPair> reload(Path path) throws IOException { |
| try { |
| List<KnownHostEntry> rawEntries = KnownHostEntryReader |
| .readFromFile(path); |
| updateReloadAttributes(); |
| if (rawEntries == null || rawEntries.isEmpty()) { |
| return Collections.emptyList(); |
| } |
| List<HostEntryPair> newEntries = new LinkedList<>(); |
| for (KnownHostEntry entry : rawEntries) { |
| AuthorizedKeyEntry keyPart = entry.getKeyEntry(); |
| if (keyPart == null) { |
| continue; |
| } |
| try { |
| PublicKey serverKey = keyPart.resolvePublicKey(null, |
| PublicKeyEntryResolver.IGNORING); |
| if (serverKey == null) { |
| LOG.warn(format( |
| SshdText.get().knownHostsUnknownKeyType, |
| path, entry.getConfigLine())); |
| } else { |
| newEntries.add(new HostEntryPair(entry, serverKey)); |
| } |
| } catch (GeneralSecurityException e) { |
| LOG.warn(format(SshdText.get().knownHostsInvalidLine, |
| path, entry.getConfigLine())); |
| } |
| } |
| return newEntries; |
| } catch (FileNotFoundException e) { |
| resetReloadAttributes(); |
| return Collections.emptyList(); |
| } |
| } |
| } |
| |
| // The stuff below is just a hack to avoid having to copy a lot of code from |
| // KnownHostsServerKeyVerifier |
| |
| private static class HostKeyHelper extends KnownHostsServerKeyVerifier { |
| |
| public HostKeyHelper() { |
| // These two arguments will never be used in any way. |
| super((c, r, s) -> false, new File(".").toPath()); //$NON-NLS-1$ |
| } |
| |
| @Override |
| protected KnownHostEntry prepareKnownHostEntry( |
| ClientSession clientSession, SocketAddress remoteAddress, |
| PublicKey serverKey) throws IOException { |
| // Make this method accessible |
| try { |
| return super.prepareKnownHostEntry(clientSession, remoteAddress, |
| serverKey); |
| } catch (Exception e) { |
| throw new IOException(e.getMessage(), e); |
| } |
| } |
| |
| @Override |
| protected String prepareModifiedServerKeyLine( |
| ClientSession clientSession, SocketAddress remoteAddress, |
| KnownHostEntry entry, String curLine, PublicKey expected, |
| PublicKey actual) throws IOException { |
| // Make this method accessible |
| try { |
| return super.prepareModifiedServerKeyLine(clientSession, |
| remoteAddress, entry, curLine, expected, actual); |
| } catch (Exception e) { |
| throw new IOException(e.getMessage(), e); |
| } |
| } |
| |
| @Override |
| protected Collection<SshdSocketAddress> resolveHostNetworkIdentities( |
| ClientSession clientSession, SocketAddress remoteAddress) { |
| // Make this method accessible |
| return super.resolveHostNetworkIdentities(clientSession, |
| remoteAddress); |
| } |
| } |
| |
| } |