-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathHttpDelegatingCredentialsProvider.java
184 lines (158 loc) · 7.78 KB
/
HttpDelegatingCredentialsProvider.java
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
package io.github.gitflowincrementalbuilder.jgit;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.transport.CredentialItem;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.FS.ExecutionResult;
import org.eclipse.jgit.util.TemporaryBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* JGit-{@link CredentialsProvider} for HTTP(S) that is delegating all credential requests to native Git via {@code git credential fill}. This will consult
* all configured credential helpers, if any (for the repo, the user and the system). Such a helper might query the user for the credentials in case it
* cannot yet provide them. However, the assumption here is that the credentials should already exist. Therefore this provider does <i>not</i> give feedback
* to native Git via {@code git credential approve} or {@code git credential verify}.
* <p>
* This provider will suppress any console input requests (see
* <a href="https://git-scm.com/docs/git#Documentation/git.txt-codeGITTERMINALPROMPTcode">GIT_TERMINAL_PROMPT</a>).
* </p>
*
* @see <a href="https://git-scm.com/docs/git-credential">Git documentation: git credential</a>
*/
class HttpDelegatingCredentialsProvider extends CredentialsProvider {
private Logger logger = LoggerFactory.getLogger(HttpDelegatingCredentialsProvider.class);
private final Path projectDir;
private final Map<String, String> additionalNativeGitEnvironment;
private final Map<URIish, CredentialsPair> credentials = new HashMap<>();
public HttpDelegatingCredentialsProvider(Path projectDir, Map<String, String> additionalNativeGitEnvironment) {
this.projectDir = projectDir;
this.additionalNativeGitEnvironment = new HashMap<>(additionalNativeGitEnvironment);
}
@Override
public boolean isInteractive() {
// possibly interactive in case some credential helper asks for input
return true;
}
@Override
public boolean supports(CredentialItem... items) {
return Arrays.stream(items)
.allMatch(item -> item instanceof CredentialItem.Username || item instanceof CredentialItem.Password);
}
@Override
public boolean get(URIish uri, CredentialItem... items) throws UnsupportedCredentialItem {
// only handle HTTP(s)
if (uri.getScheme() != null && !uri.getScheme().startsWith("http")) {
return false;
}
CredentialsPair credentialsPair = credentials.computeIfAbsent(uri, u -> {
try {
return lookupCredentials(uri);
} catch (IOException | InterruptedException | RuntimeException e) {
logger.warn("Failed to look up credentials via 'git credential fill' for: " + uri, e);
return null;
}
});
if (credentialsPair == null) {
return false;
}
// map extracted credentials to CredentialItems, see also: org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider
for (CredentialItem item : items) {
if (item instanceof CredentialItem.Username) {
((CredentialItem.Username) item).setValue(credentialsPair.username);
} else if (item instanceof CredentialItem.Password) {
((CredentialItem.Password) item).setValue(credentialsPair.password);
} else if (item instanceof CredentialItem.StringType && item.getPromptText().equals("Password: ")) {
((CredentialItem.StringType) item).setValue(new String(credentialsPair.password));
} else {
throw new UnsupportedCredentialItem(uri, item.getClass().getName() + ":" + item.getPromptText());
}
}
return true;
}
@Override
// see also: org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider.clear()
public void reset(URIish uri) {
Optional.ofNullable(credentials.remove(uri))
.ifPresent(credPair -> {
credPair.username = null;
Arrays.fill(credPair.password, (char) 0);
credPair.password = null;
});
}
public void resetAll() {
new HashSet<>(credentials.keySet()).forEach(this::reset);
}
private CredentialsPair lookupCredentials(URIish uri) throws IOException, InterruptedException {
// utilize JGit command execution capabilities
FS fs = FS.detect();
ProcessBuilder procBuilder = fs.runInShell("git", new String[] {"credential", "fill"});
// prevent native git from requesting console input (not implemented)
procBuilder.environment().put("GIT_TERMINAL_PROMPT", "0");
// add additional environment entries, if present (test only)
if (!additionalNativeGitEnvironment.isEmpty()) {
procBuilder.environment().putAll(additionalNativeGitEnvironment);
}
procBuilder.directory(projectDir.toFile());
ExecutionResult result = fs.execute(procBuilder, new ByteArrayInputStream(buildGitCommandInput(uri).getBytes(Charset.defaultCharset())));
if (result.getRc() != 0) {
logger.info(bufferToString(result.getStdout()));
logger.error(bufferToString(result.getStderr()));
throw new IllegalStateException("Native Git invocation failed with return code " + result.getRc()
+ ". See previous log output for more details.");
}
return extractCredentials(bufferToString(result.getStdout()));
}
// build input for "git credential fill" as per https://git-scm.com/docs/git-credential#_typical_use_of_git_credential
private String buildGitCommandInput(URIish uri) {
StringBuilder builder = new StringBuilder();
builder.append("protocol=").append(uri.getScheme()).append("\n");
builder.append("host=").append(uri.getHost());
if (uri.getPort() != -1) {
builder.append(":").append(uri.getPort());
}
builder.append("\n");
Optional.ofNullable(uri.getPath())
.map(path -> path.startsWith("/") ? path.substring(1) : path)
.ifPresent(path -> builder.append("path=").append(path).append("\n"));
Optional.ofNullable(uri.getUser())
.ifPresent(user -> builder.append("username=").append(user).append("\n"));
return builder.toString();
}
private String bufferToString(TemporaryBuffer buffer) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
buffer.writeTo(baos, null);
return baos.toString(Charset.defaultCharset().name());
}
private CredentialsPair extractCredentials(String nativeGitOutput) {
Matcher matcher = Pattern.compile("(?<=username=).+|(?<=password=).+").matcher(nativeGitOutput);
if (!matcher.find()) {
throw new IllegalStateException("Could not find username in native Git output");
}
String username = matcher.group();
if (!matcher.find()) {
throw new IllegalStateException("Could not find password in native Git output");
}
char[] password = matcher.group().toCharArray();
CredentialsPair credPair = new CredentialsPair();
credPair.username = username;
credPair.password = password;
return credPair;
}
private static class CredentialsPair {
private String username;
private char[] password;
}
}