You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

LfsConnectionFactory.java 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. /*
  2. * Copyright (C) 2017, Markus Duft <markus.duft@ssi-schaefer.com> 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.lfs.internal;
  11. import static org.eclipse.jgit.util.HttpSupport.ENCODING_GZIP;
  12. import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT;
  13. import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT_ENCODING;
  14. import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_TYPE;
  15. import java.io.IOException;
  16. import java.net.ProxySelector;
  17. import java.net.URISyntaxException;
  18. import java.net.URL;
  19. import java.time.LocalDateTime;
  20. import java.time.ZoneOffset;
  21. import java.time.format.DateTimeFormatter;
  22. import java.util.LinkedList;
  23. import java.util.Map;
  24. import java.util.TreeMap;
  25. import org.eclipse.jgit.annotations.NonNull;
  26. import org.eclipse.jgit.errors.CommandFailedException;
  27. import org.eclipse.jgit.lfs.LfsPointer;
  28. import org.eclipse.jgit.lfs.Protocol;
  29. import org.eclipse.jgit.lfs.errors.LfsConfigInvalidException;
  30. import org.eclipse.jgit.lib.ConfigConstants;
  31. import org.eclipse.jgit.lib.Repository;
  32. import org.eclipse.jgit.lib.StoredConfig;
  33. import org.eclipse.jgit.transport.HttpConfig;
  34. import org.eclipse.jgit.transport.HttpTransport;
  35. import org.eclipse.jgit.transport.URIish;
  36. import org.eclipse.jgit.transport.http.HttpConnection;
  37. import org.eclipse.jgit.util.HttpSupport;
  38. import org.eclipse.jgit.util.SshSupport;
  39. /**
  40. * Provides means to get a valid LFS connection for a given repository.
  41. */
  42. public class LfsConnectionFactory {
  43. private static final int SSH_AUTH_TIMEOUT_SECONDS = 30;
  44. private static final String SCHEME_HTTPS = "https"; //$NON-NLS-1$
  45. private static final String SCHEME_SSH = "ssh"; //$NON-NLS-1$
  46. private static final Map<String, AuthCache> sshAuthCache = new TreeMap<>();
  47. /**
  48. * Determine URL of LFS server by looking into config parameters lfs.url,
  49. * lfs.[remote].url or remote.[remote].url. The LFS server URL is computed
  50. * from remote.[remote].url by appending "/info/lfs". In case there is no
  51. * URL configured, a SSH remote URI can be used to auto-detect the LFS URI
  52. * by using the remote "git-lfs-authenticate" command.
  53. *
  54. * @param db
  55. * the repository to work with
  56. * @param method
  57. * the method (GET,PUT,...) of the request this connection will
  58. * be used for
  59. * @param purpose
  60. * the action, e.g. Protocol.OPERATION_DOWNLOAD
  61. * @return the url for the lfs server. e.g.
  62. * "https://github.com/github/git-lfs.git/info/lfs"
  63. * @throws IOException
  64. */
  65. public static HttpConnection getLfsConnection(Repository db, String method,
  66. String purpose) throws IOException {
  67. StoredConfig config = db.getConfig();
  68. Map<String, String> additionalHeaders = new TreeMap<>();
  69. String lfsUrl = getLfsUrl(db, purpose, additionalHeaders);
  70. URL url = new URL(lfsUrl + Protocol.OBJECTS_LFS_ENDPOINT);
  71. HttpConnection connection = HttpTransport.getConnectionFactory().create(
  72. url, HttpSupport.proxyFor(ProxySelector.getDefault(), url));
  73. connection.setDoOutput(true);
  74. if (url.getProtocol().equals(SCHEME_HTTPS)
  75. && !config.getBoolean(HttpConfig.HTTP,
  76. HttpConfig.SSL_VERIFY_KEY, true)) {
  77. HttpSupport.disableSslVerify(connection);
  78. }
  79. connection.setRequestMethod(method);
  80. connection.setRequestProperty(HDR_ACCEPT,
  81. Protocol.CONTENTTYPE_VND_GIT_LFS_JSON);
  82. connection.setRequestProperty(HDR_CONTENT_TYPE,
  83. Protocol.CONTENTTYPE_VND_GIT_LFS_JSON);
  84. additionalHeaders
  85. .forEach((k, v) -> connection.setRequestProperty(k, v));
  86. return connection;
  87. }
  88. private static String getLfsUrl(Repository db, String purpose,
  89. Map<String, String> additionalHeaders)
  90. throws LfsConfigInvalidException {
  91. StoredConfig config = db.getConfig();
  92. String lfsUrl = config.getString(ConfigConstants.CONFIG_SECTION_LFS,
  93. null,
  94. ConfigConstants.CONFIG_KEY_URL);
  95. Exception ex = null;
  96. if (lfsUrl == null) {
  97. String remoteUrl = null;
  98. for (String remote : db.getRemoteNames()) {
  99. lfsUrl = config.getString(ConfigConstants.CONFIG_SECTION_LFS,
  100. remote,
  101. ConfigConstants.CONFIG_KEY_URL);
  102. // This could be done better (more precise logic), but according
  103. // to https://github.com/git-lfs/git-lfs/issues/1759 git-lfs
  104. // generally only supports 'origin' in an integrated workflow.
  105. if (lfsUrl == null && (remote.equals(
  106. org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME))) {
  107. remoteUrl = config.getString(
  108. ConfigConstants.CONFIG_KEY_REMOTE, remote,
  109. ConfigConstants.CONFIG_KEY_URL);
  110. break;
  111. }
  112. }
  113. if (lfsUrl == null && remoteUrl != null) {
  114. try {
  115. lfsUrl = discoverLfsUrl(db, purpose, additionalHeaders,
  116. remoteUrl);
  117. } catch (URISyntaxException | IOException
  118. | CommandFailedException e) {
  119. ex = e;
  120. }
  121. } else {
  122. lfsUrl = lfsUrl + Protocol.INFO_LFS_ENDPOINT;
  123. }
  124. }
  125. if (lfsUrl == null) {
  126. if (ex != null) {
  127. throw new LfsConfigInvalidException(
  128. LfsText.get().lfsNoDownloadUrl, ex);
  129. }
  130. throw new LfsConfigInvalidException(LfsText.get().lfsNoDownloadUrl);
  131. }
  132. return lfsUrl;
  133. }
  134. private static String discoverLfsUrl(Repository db, String purpose,
  135. Map<String, String> additionalHeaders, String remoteUrl)
  136. throws URISyntaxException, IOException, CommandFailedException {
  137. URIish u = new URIish(remoteUrl);
  138. if (u.getScheme() == null || SCHEME_SSH.equals(u.getScheme())) {
  139. Protocol.ExpiringAction action = getSshAuthentication(db, purpose,
  140. remoteUrl, u);
  141. additionalHeaders.putAll(action.header);
  142. return action.href;
  143. }
  144. return remoteUrl + Protocol.INFO_LFS_ENDPOINT;
  145. }
  146. private static Protocol.ExpiringAction getSshAuthentication(
  147. Repository db, String purpose, String remoteUrl, URIish u)
  148. throws IOException, CommandFailedException {
  149. AuthCache cached = sshAuthCache.get(remoteUrl);
  150. Protocol.ExpiringAction action = null;
  151. if (cached != null && cached.validUntil > System.currentTimeMillis()) {
  152. action = cached.cachedAction;
  153. }
  154. if (action == null) {
  155. // discover and authenticate; git-lfs does "ssh
  156. // -p <port> -- <host> git-lfs-authenticate
  157. // <project> <upload/download>"
  158. String json = SshSupport.runSshCommand(u.setPath(""), //$NON-NLS-1$
  159. null, db.getFS(),
  160. "git-lfs-authenticate " + extractProjectName(u) + " " //$NON-NLS-1$//$NON-NLS-2$
  161. + purpose,
  162. SSH_AUTH_TIMEOUT_SECONDS);
  163. action = Protocol.gson().fromJson(json,
  164. Protocol.ExpiringAction.class);
  165. // cache the result as long as possible.
  166. AuthCache c = new AuthCache(action);
  167. sshAuthCache.put(remoteUrl, c);
  168. }
  169. return action;
  170. }
  171. /**
  172. * Create a connection for the specified
  173. * {@link org.eclipse.jgit.lfs.Protocol.Action}.
  174. *
  175. * @param repo
  176. * the repo to fetch required configuration from
  177. * @param action
  178. * the action for which to create a connection
  179. * @param method
  180. * the target method (GET or PUT)
  181. * @return a connection. output mode is not set.
  182. * @throws IOException
  183. * in case of any error.
  184. */
  185. @NonNull
  186. public static HttpConnection getLfsContentConnection(
  187. Repository repo, Protocol.Action action, String method)
  188. throws IOException {
  189. URL contentUrl = new URL(action.href);
  190. HttpConnection contentServerConn = HttpTransport.getConnectionFactory()
  191. .create(contentUrl, HttpSupport
  192. .proxyFor(ProxySelector.getDefault(), contentUrl));
  193. contentServerConn.setRequestMethod(method);
  194. if (action.header != null) {
  195. action.header.forEach(
  196. (k, v) -> contentServerConn.setRequestProperty(k, v));
  197. }
  198. if (contentUrl.getProtocol().equals(SCHEME_HTTPS)
  199. && !repo.getConfig().getBoolean(HttpConfig.HTTP,
  200. HttpConfig.SSL_VERIFY_KEY, true)) {
  201. HttpSupport.disableSslVerify(contentServerConn);
  202. }
  203. contentServerConn.setRequestProperty(HDR_ACCEPT_ENCODING,
  204. ENCODING_GZIP);
  205. return contentServerConn;
  206. }
  207. private static String extractProjectName(URIish u) {
  208. String path = u.getPath();
  209. // begins with a slash if the url contains a port (gerrit vs. github).
  210. if (path.startsWith("/")) { //$NON-NLS-1$
  211. path = path.substring(1);
  212. }
  213. if (path.endsWith(org.eclipse.jgit.lib.Constants.DOT_GIT)) {
  214. return path.substring(0, path.length() - 4);
  215. }
  216. return path;
  217. }
  218. /**
  219. * @param operation
  220. * the operation to perform, e.g. Protocol.OPERATION_DOWNLOAD
  221. * @param resources
  222. * the LFS resources affected
  223. * @return a request that can be serialized to JSON
  224. */
  225. public static Protocol.Request toRequest(String operation,
  226. LfsPointer... resources) {
  227. Protocol.Request req = new Protocol.Request();
  228. req.operation = operation;
  229. if (resources != null) {
  230. req.objects = new LinkedList<>();
  231. for (LfsPointer res : resources) {
  232. Protocol.ObjectSpec o = new Protocol.ObjectSpec();
  233. o.oid = res.getOid().getName();
  234. o.size = res.getSize();
  235. req.objects.add(o);
  236. }
  237. }
  238. return req;
  239. }
  240. private static final class AuthCache {
  241. private static final long AUTH_CACHE_EAGER_TIMEOUT = 500;
  242. private static final DateTimeFormatter ISO_FORMAT = DateTimeFormatter
  243. .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX"); //$NON-NLS-1$
  244. /**
  245. * Creates a cache entry for an authentication response.
  246. * <p>
  247. * The timeout of the cache token is extracted from the given action. If
  248. * no timeout can be determined, the token will be used only once.
  249. *
  250. * @param action
  251. */
  252. public AuthCache(Protocol.ExpiringAction action) {
  253. this.cachedAction = action;
  254. try {
  255. if (action.expiresIn != null && !action.expiresIn.isEmpty()) {
  256. this.validUntil = (System.currentTimeMillis()
  257. + Long.parseLong(action.expiresIn))
  258. - AUTH_CACHE_EAGER_TIMEOUT;
  259. } else if (action.expiresAt != null
  260. && !action.expiresAt.isEmpty()) {
  261. this.validUntil = LocalDateTime
  262. .parse(action.expiresAt, ISO_FORMAT)
  263. .atZone(ZoneOffset.UTC).toInstant().toEpochMilli()
  264. - AUTH_CACHE_EAGER_TIMEOUT;
  265. } else {
  266. this.validUntil = System.currentTimeMillis();
  267. }
  268. } catch (Exception e) {
  269. this.validUntil = System.currentTimeMillis();
  270. }
  271. }
  272. long validUntil;
  273. Protocol.ExpiringAction cachedAction;
  274. }
  275. }