JGitUserInteraction.java

  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. import java.net.InetSocketAddress;
  12. import java.util.ArrayList;
  13. import java.util.List;
  14. import java.util.Map;
  15. import java.util.concurrent.ConcurrentHashMap;

  16. import org.apache.sshd.client.auth.keyboard.UserInteraction;
  17. import org.apache.sshd.client.session.ClientSession;
  18. import org.apache.sshd.common.session.Session;
  19. import org.apache.sshd.common.session.SessionListener;
  20. import org.eclipse.jgit.transport.CredentialItem;
  21. import org.eclipse.jgit.transport.CredentialsProvider;
  22. import org.eclipse.jgit.transport.SshConstants;
  23. import org.eclipse.jgit.transport.URIish;

  24. /**
  25.  * A {@link UserInteraction} callback implementation based on a
  26.  * {@link CredentialsProvider}.
  27.  */
  28. public class JGitUserInteraction implements UserInteraction {

  29.     private final CredentialsProvider provider;

  30.     /**
  31.      * We need to reset the JGit credentials provider if we have repeated
  32.      * attempts.
  33.      */
  34.     private final Map<Session, SessionListener> ongoing = new ConcurrentHashMap<>();

  35.     /**
  36.      * Creates a new {@link JGitUserInteraction} for interactive password input
  37.      * based on the given {@link CredentialsProvider}.
  38.      *
  39.      * @param provider
  40.      *            to use
  41.      */
  42.     public JGitUserInteraction(CredentialsProvider provider) {
  43.         this.provider = provider;
  44.     }

  45.     @Override
  46.     public boolean isInteractionAllowed(ClientSession session) {
  47.         return provider != null && provider.isInteractive();
  48.     }

  49.     @Override
  50.     public String[] interactive(ClientSession session, String name,
  51.             String instruction, String lang, String[] prompt, boolean[] echo) {
  52.         // This is keyboard-interactive or password authentication
  53.         List<CredentialItem> items = new ArrayList<>();
  54.         int numberOfHiddenInputs = 0;
  55.         for (int i = 0; i < prompt.length; i++) {
  56.             boolean hidden = i < echo.length && !echo[i];
  57.             if (hidden) {
  58.                 numberOfHiddenInputs++;
  59.             }
  60.         }
  61.         // RFC 4256 (SSH_MSG_USERAUTH_INFO_REQUEST) says: "The language tag is
  62.         // deprecated and SHOULD be the empty string." and "[If there are no
  63.         // prompts] the client SHOULD still display the name and instruction
  64.         // fields" and "[The] client SHOULD print the name and instruction (if
  65.         // non-empty)"
  66.         if (name != null && !name.isEmpty()) {
  67.             items.add(new CredentialItem.InformationalMessage(name));
  68.         }
  69.         if (instruction != null && !instruction.isEmpty()) {
  70.             items.add(new CredentialItem.InformationalMessage(instruction));
  71.         }
  72.         for (int i = 0; i < prompt.length; i++) {
  73.             boolean hidden = i < echo.length && !echo[i];
  74.             if (hidden && numberOfHiddenInputs == 1) {
  75.                 // We need to somehow trigger storing the password in the
  76.                 // Eclipse secure storage in EGit. Currently, this is done only
  77.                 // for password fields.
  78.                 items.add(new CredentialItem.Password());
  79.                 // TODO Possibly change EGit to store all hidden strings
  80.                 // (keyed by the URI and the prompt?) so that we don't have to
  81.                 // use this kludge here.
  82.             } else {
  83.                 items.add(new CredentialItem.StringType(prompt[i], hidden));
  84.             }
  85.         }
  86.         if (items.isEmpty()) {
  87.             // Huh? No info, no prompts?
  88.             return prompt; // Is known to have length zero here
  89.         }
  90.         URIish uri = toURI(session.getUsername(),
  91.                 (InetSocketAddress) session.getConnectAddress());
  92.         // Reset the provider for this URI if it's not the first attempt and we
  93.         // have hidden inputs. Otherwise add a session listener that will remove
  94.         // itself once authenticated.
  95.         if (numberOfHiddenInputs > 0) {
  96.             SessionListener listener = ongoing.get(session);
  97.             if (listener != null) {
  98.                 provider.reset(uri);
  99.             } else {
  100.                 listener = new SessionAuthMarker(ongoing);
  101.                 ongoing.put(session, listener);
  102.                 session.addSessionListener(listener);
  103.             }
  104.         }
  105.         if (provider.get(uri, items)) {
  106.             return items.stream().map(i -> {
  107.                 if (i instanceof CredentialItem.Password) {
  108.                     return new String(((CredentialItem.Password) i).getValue());
  109.                 } else if (i instanceof CredentialItem.StringType) {
  110.                     return ((CredentialItem.StringType) i).getValue();
  111.                 }
  112.                 return null;
  113.             }).filter(s -> s != null).toArray(String[]::new);
  114.         }
  115.         // TODO What to throw to abort the connection/authentication process?
  116.         // In UserAuthKeyboardInteractive.getUserResponses() it's clear that
  117.         // returning null is valid and signifies "an error"; we'll try the
  118.         // next authentication method. But if the user explicitly canceled,
  119.         // then we don't want to try the next methods...
  120.         //
  121.         // Probably not a serious issue with the typical order of public-key,
  122.         // keyboard-interactive, password.
  123.         return null;
  124.     }

  125.     @Override
  126.     public String getUpdatedPassword(ClientSession session, String prompt,
  127.             String lang) {
  128.         // TODO Implement password update in password authentication?
  129.         return null;
  130.     }

  131.     /**
  132.      * Creates a {@link URIish} from the given remote address and user name.
  133.      *
  134.      * @param userName
  135.      *            for the uri
  136.      * @param remote
  137.      *            address of the remote host
  138.      * @return the uri, with {@link SshConstants#SSH_SCHEME} as scheme
  139.      */
  140.     public static URIish toURI(String userName, InetSocketAddress remote) {
  141.         String host = remote.getHostString();
  142.         int port = remote.getPort();
  143.         return new URIish() //
  144.                 .setScheme(SshConstants.SSH_SCHEME) //
  145.                 .setHost(host) //
  146.                 .setPort(port) //
  147.                 .setUser(userName);
  148.     }

  149.     /**
  150.      * A {@link SessionListener} that removes itself from the session when
  151.      * authentication is done or the session is closed.
  152.      */
  153.     private static class SessionAuthMarker implements SessionListener {

  154.         private final Map<Session, SessionListener> registered;

  155.         public SessionAuthMarker(Map<Session, SessionListener> registered) {
  156.             this.registered = registered;
  157.         }

  158.         @Override
  159.         public void sessionEvent(Session session, SessionListener.Event event) {
  160.             if (event == SessionListener.Event.Authenticated) {
  161.                 session.removeSessionListener(this);
  162.                 registered.remove(session, this);
  163.             }
  164.         }

  165.         @Override
  166.         public void sessionClosed(Session session) {
  167.             session.removeSessionListener(this);
  168.             registered.remove(session, this);
  169.         }
  170.     }
  171. }