aboutsummaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java
blob: 2a725ea16ad281c93e54404ced903d88793b8e58 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
/*
 * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0 which is available at
 * https://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */
package org.eclipse.jgit.internal.transport.sshd;

import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.sshd.client.auth.keyboard.UserInteraction;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.session.Session;
import org.apache.sshd.common.session.SessionListener;
import org.eclipse.jgit.transport.CredentialItem;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.SshConstants;
import org.eclipse.jgit.transport.URIish;

/**
 * A {@link UserInteraction} callback implementation based on a
 * {@link CredentialsProvider}.
 */
public class JGitUserInteraction implements UserInteraction {

	private final CredentialsProvider provider;

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

	/**
	 * Creates a new {@link JGitUserInteraction} for interactive password input
	 * based on the given {@link CredentialsProvider}.
	 *
	 * @param provider
	 *            to use
	 */
	public JGitUserInteraction(CredentialsProvider provider) {
		this.provider = provider;
	}

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

	@Override
	public String[] interactive(ClientSession session, String name,
			String instruction, String lang, String[] prompt, boolean[] echo) {
		// This is keyboard-interactive or password authentication
		List<CredentialItem> items = new ArrayList<>();
		int numberOfHiddenInputs = 0;
		for (int i = 0; i < prompt.length; i++) {
			boolean hidden = i < echo.length && !echo[i];
			if (hidden) {
				numberOfHiddenInputs++;
			}
		}
		// RFC 4256 (SSH_MSG_USERAUTH_INFO_REQUEST) says: "The language tag is
		// deprecated and SHOULD be the empty string." and "[If there are no
		// prompts] the client SHOULD still display the name and instruction
		// fields" and "[The] client SHOULD print the name and instruction (if
		// non-empty)"
		if (name != null && !name.isEmpty()) {
			items.add(new CredentialItem.InformationalMessage(name));
		}
		if (instruction != null && !instruction.isEmpty()) {
			items.add(new CredentialItem.InformationalMessage(instruction));
		}
		for (int i = 0; i < prompt.length; i++) {
			boolean hidden = i < echo.length && !echo[i];
			if (hidden && numberOfHiddenInputs == 1) {
				// We need to somehow trigger storing the password in the
				// Eclipse secure storage in EGit. Currently, this is done only
				// for password fields.
				items.add(new CredentialItem.Password());
				// TODO Possibly change EGit to store all hidden strings
				// (keyed by the URI and the prompt?) so that we don't have to
				// use this kludge here.
			} else {
				items.add(new CredentialItem.StringType(prompt[i], hidden));
			}
		}
		if (items.isEmpty()) {
			// Huh? No info, no prompts?
			return prompt; // Is known to have length zero here
		}
		URIish uri = toURI(session.getUsername(),
				(InetSocketAddress) session.getConnectAddress());
		// Reset the provider for this URI if it's not the first attempt and we
		// have hidden inputs. Otherwise add a session listener that will remove
		// itself once authenticated.
		if (numberOfHiddenInputs > 0) {
			SessionListener listener = ongoing.get(session);
			if (listener != null) {
				provider.reset(uri);
			} else {
				listener = new SessionAuthMarker(ongoing);
				ongoing.put(session, listener);
				session.addSessionListener(listener);
			}
		}
		if (provider.get(uri, items)) {
			return items.stream().map(i -> {
				if (i instanceof CredentialItem.Password) {
					return new String(((CredentialItem.Password) i).getValue());
				} else if (i instanceof CredentialItem.StringType) {
					return ((CredentialItem.StringType) i).getValue();
				}
				return null;
			}).filter(s -> s != null).toArray(String[]::new);
		}
		throw new AuthenticationCanceledException();
	}

	@Override
	public String resolveAuthPasswordAttempt(ClientSession session)
			throws Exception {
		String[] results = interactive(session, null, null, "", //$NON-NLS-1$
				new String[] { SshdText.get().passwordPrompt },
				new boolean[] { false });
		return (results == null || results.length == 0) ? null : results[0];
	}

	@Override
	public String getUpdatedPassword(ClientSession session, String prompt,
			String lang) {
		// TODO Implement password update in password authentication?
		return null;
	}

	/**
	 * Creates a {@link URIish} from the given remote address and user name.
	 *
	 * @param userName
	 *            for the uri
	 * @param remote
	 *            address of the remote host
	 * @return the uri, with {@link SshConstants#SSH_SCHEME} as scheme
	 */
	public static URIish toURI(String userName, InetSocketAddress remote) {
		String host = remote.getHostString();
		int port = remote.getPort();
		return new URIish() //
				.setScheme(SshConstants.SSH_SCHEME) //
				.setHost(host) //
				.setPort(port) //
				.setUser(userName);
	}

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

		private final Map<Session, SessionListener> registered;

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

		@Override
		public void sessionEvent(Session session, SessionListener.Event event) {
			if (event == SessionListener.Event.Authenticated) {
				session.removeSessionListener(this);
				registered.remove(session, this);
			}
		}

		@Override
		public void sessionClosed(Session session) {
			session.removeSessionListener(this);
			registered.remove(session, this);
		}
	}
}