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 package org.eclipse.jgit.lfs.internal;
44
45 import static org.eclipse.jgit.util.HttpSupport.ENCODING_GZIP;
46 import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT;
47 import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT_ENCODING;
48 import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_TYPE;
49
50 import java.io.IOException;
51 import java.net.ProxySelector;
52 import java.net.URISyntaxException;
53 import java.net.URL;
54 import java.text.SimpleDateFormat;
55 import java.util.LinkedList;
56 import java.util.Map;
57 import java.util.TreeMap;
58
59 import org.eclipse.jgit.annotations.NonNull;
60 import org.eclipse.jgit.errors.CommandFailedException;
61 import org.eclipse.jgit.lfs.LfsPointer;
62 import org.eclipse.jgit.lfs.Protocol;
63 import org.eclipse.jgit.lfs.errors.LfsConfigInvalidException;
64 import org.eclipse.jgit.lib.ConfigConstants;
65 import org.eclipse.jgit.lib.Repository;
66 import org.eclipse.jgit.lib.StoredConfig;
67 import org.eclipse.jgit.transport.HttpConfig;
68 import org.eclipse.jgit.transport.HttpTransport;
69 import org.eclipse.jgit.transport.URIish;
70 import org.eclipse.jgit.transport.http.HttpConnection;
71 import org.eclipse.jgit.util.HttpSupport;
72 import org.eclipse.jgit.util.SshSupport;
73
74
75
76
77 public class LfsConnectionFactory {
78
79 private static final int SSH_AUTH_TIMEOUT_SECONDS = 30;
80 private static final String SCHEME_HTTPS = "https";
81 private static final String SCHEME_SSH = "ssh";
82 private static final Map<String, AuthCache> sshAuthCache = new TreeMap<>();
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102 public static HttpConnection getLfsConnection(Repository db, String method,
103 String purpose) throws IOException {
104 StoredConfig config = db.getConfig();
105 Map<String, String> additionalHeaders = new TreeMap<>();
106 String lfsUrl = getLfsUrl(db, purpose, additionalHeaders);
107 URL url = new URL(lfsUrl + Protocol.OBJECTS_LFS_ENDPOINT);
108 HttpConnection connection = HttpTransport.getConnectionFactory().create(
109 url, HttpSupport.proxyFor(ProxySelector.getDefault(), url));
110 connection.setDoOutput(true);
111 if (url.getProtocol().equals(SCHEME_HTTPS)
112 && !config.getBoolean(HttpConfig.HTTP,
113 HttpConfig.SSL_VERIFY_KEY, true)) {
114 HttpSupport.disableSslVerify(connection);
115 }
116 connection.setRequestMethod(method);
117 connection.setRequestProperty(HDR_ACCEPT,
118 Protocol.CONTENTTYPE_VND_GIT_LFS_JSON);
119 connection.setRequestProperty(HDR_CONTENT_TYPE,
120 Protocol.CONTENTTYPE_VND_GIT_LFS_JSON);
121 additionalHeaders
122 .forEach((k, v) -> connection.setRequestProperty(k, v));
123 return connection;
124 }
125
126 private static String getLfsUrl(Repository db, String purpose,
127 Map<String, String> additionalHeaders)
128 throws LfsConfigInvalidException {
129 StoredConfig config = db.getConfig();
130 String lfsUrl = config.getString(ConfigConstants.CONFIG_SECTION_LFS,
131 null,
132 ConfigConstants.CONFIG_KEY_URL);
133 Exception ex = null;
134 if (lfsUrl == null) {
135 String remoteUrl = null;
136 for (String remote : db.getRemoteNames()) {
137 lfsUrl = config.getString(ConfigConstants.CONFIG_SECTION_LFS,
138 remote,
139 ConfigConstants.CONFIG_KEY_URL);
140
141
142
143 if (lfsUrl == null && (remote.equals(
144 org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME))) {
145 remoteUrl = config.getString(
146 ConfigConstants.CONFIG_KEY_REMOTE, remote,
147 ConfigConstants.CONFIG_KEY_URL);
148 }
149 break;
150 }
151 if (lfsUrl == null && remoteUrl != null) {
152 try {
153 lfsUrl = discoverLfsUrl(db, purpose, additionalHeaders,
154 remoteUrl);
155 } catch (URISyntaxException | IOException
156 | CommandFailedException e) {
157 ex = e;
158 }
159 } else {
160 lfsUrl = lfsUrl + Protocol.INFO_LFS_ENDPOINT;
161 }
162 }
163 if (lfsUrl == null) {
164 if (ex != null) {
165 throw new LfsConfigInvalidException(
166 LfsText.get().lfsNoDownloadUrl, ex);
167 }
168 throw new LfsConfigInvalidException(LfsText.get().lfsNoDownloadUrl);
169 }
170 return lfsUrl;
171 }
172
173 private static String discoverLfsUrl(Repository db, String purpose,
174 Map<String, String> additionalHeaders, String remoteUrl)
175 throws URISyntaxException, IOException, CommandFailedException {
176 URIish u = new URIish(remoteUrl);
177 if (u.getScheme() == null || SCHEME_SSH.equals(u.getScheme())) {
178 Protocol.ExpiringAction action = getSshAuthentication(db, purpose,
179 remoteUrl, u);
180 additionalHeaders.putAll(action.header);
181 return action.href;
182 } else {
183 return remoteUrl + Protocol.INFO_LFS_ENDPOINT;
184 }
185 }
186
187 private static Protocol.ExpiringAction getSshAuthentication(
188 Repository db, String purpose, String remoteUrl, URIish u)
189 throws IOException, CommandFailedException {
190 AuthCache cached = sshAuthCache.get(remoteUrl);
191 Protocol.ExpiringAction action = null;
192 if (cached != null && cached.validUntil > System.currentTimeMillis()) {
193 action = cached.cachedAction;
194 }
195
196 if (action == null) {
197
198
199
200 String json = SshSupport.runSshCommand(u.setPath(""),
201 null, db.getFS(),
202 "git-lfs-authenticate " + extractProjectName(u) + " "
203 + purpose,
204 SSH_AUTH_TIMEOUT_SECONDS);
205
206 action = Protocol.gson().fromJson(json,
207 Protocol.ExpiringAction.class);
208
209
210 AuthCache c = new AuthCache(action);
211 sshAuthCache.put(remoteUrl, c);
212 }
213 return action;
214 }
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230 @NonNull
231 public static HttpConnection getLfsContentConnection(
232 Repository repo, Protocol.Action action, String method)
233 throws IOException {
234 URL contentUrl = new URL(action.href);
235 HttpConnection contentServerConn = HttpTransport.getConnectionFactory()
236 .create(contentUrl, HttpSupport
237 .proxyFor(ProxySelector.getDefault(), contentUrl));
238 contentServerConn.setRequestMethod(method);
239 if (action.header != null) {
240 action.header.forEach(
241 (k, v) -> contentServerConn.setRequestProperty(k, v));
242 }
243 if (contentUrl.getProtocol().equals(SCHEME_HTTPS)
244 && !repo.getConfig().getBoolean(HttpConfig.HTTP,
245 HttpConfig.SSL_VERIFY_KEY, true)) {
246 HttpSupport.disableSslVerify(contentServerConn);
247 }
248
249 contentServerConn.setRequestProperty(HDR_ACCEPT_ENCODING,
250 ENCODING_GZIP);
251
252 return contentServerConn;
253 }
254
255 private static String extractProjectName(URIish u) {
256 String path = u.getPath();
257
258
259 if (path.startsWith("/")) {
260 path = path.substring(1);
261 }
262
263 if (path.endsWith(org.eclipse.jgit.lib.Constants.DOT_GIT)) {
264 return path.substring(0, path.length() - 4);
265 } else {
266 return path;
267 }
268 }
269
270
271
272
273
274
275
276
277 public static Protocol.Request toRequest(String operation,
278 LfsPointer... resources) {
279 Protocol.Request req = new Protocol.Request();
280 req.operation = operation;
281 if (resources != null) {
282 req.objects = new LinkedList<>();
283 for (LfsPointer res : resources) {
284 Protocol.ObjectSpec o = new Protocol.ObjectSpec();
285 o.oid = res.getOid().getName();
286 o.size = res.getSize();
287 req.objects.add(o);
288 }
289 }
290 return req;
291 }
292
293 private static final class AuthCache {
294 private static final long AUTH_CACHE_EAGER_TIMEOUT = 500;
295
296 private static final SimpleDateFormat ISO_FORMAT = new SimpleDateFormat(
297 "yyyy-MM-dd'T'HH:mm:ss.SSSX");
298
299
300
301
302
303
304
305
306
307 public AuthCache(Protocol.ExpiringAction action) {
308 this.cachedAction = action;
309 try {
310 if (action.expiresIn != null && !action.expiresIn.isEmpty()) {
311 this.validUntil = (System.currentTimeMillis()
312 + Long.parseLong(action.expiresIn))
313 - AUTH_CACHE_EAGER_TIMEOUT;
314 } else if (action.expiresAt != null
315 && !action.expiresAt.isEmpty()) {
316 this.validUntil = ISO_FORMAT.parse(action.expiresAt)
317 .getTime() - AUTH_CACHE_EAGER_TIMEOUT;
318 } else {
319 this.validUntil = System.currentTimeMillis();
320 }
321 } catch (Exception e) {
322 this.validUntil = System.currentTimeMillis();
323 }
324 }
325
326 long validUntil;
327
328 Protocol.ExpiringAction cachedAction;
329 }
330
331 }