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 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. /*
  2. * Copyright (C) 2017, Markus Duft <markus.duft@ssi-schaefer.com>
  3. * and other copyright owners as documented in the project's IP log.
  4. *
  5. * This program and the accompanying materials are made available
  6. * under the terms of the Eclipse Distribution License v1.0 which
  7. * accompanies this distribution, is reproduced below, and is
  8. * available at http://www.eclipse.org/org/documents/edl-v10.php
  9. *
  10. * All rights reserved.
  11. *
  12. * Redistribution and use in source and binary forms, with or
  13. * without modification, are permitted provided that the following
  14. * conditions are met:
  15. *
  16. * - Redistributions of source code must retain the above copyright
  17. * notice, this list of conditions and the following disclaimer.
  18. *
  19. * - Redistributions in binary form must reproduce the above
  20. * copyright notice, this list of conditions and the following
  21. * disclaimer in the documentation and/or other materials provided
  22. * with the distribution.
  23. *
  24. * - Neither the name of the Eclipse Foundation, Inc. nor the
  25. * names of its contributors may be used to endorse or promote
  26. * products derived from this software without specific prior
  27. * written permission.
  28. *
  29. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  30. * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  31. * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  32. * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  33. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  34. * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  35. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  36. * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  37. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  38. * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  39. * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  40. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  41. * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  42. */
  43. package org.eclipse.jgit.lfs.internal;
  44. import static org.eclipse.jgit.util.HttpSupport.ENCODING_GZIP;
  45. import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT;
  46. import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT_ENCODING;
  47. import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_TYPE;
  48. import java.io.IOException;
  49. import java.net.ProxySelector;
  50. import java.net.URISyntaxException;
  51. import java.net.URL;
  52. import java.text.SimpleDateFormat;
  53. import java.util.LinkedList;
  54. import java.util.Map;
  55. import java.util.TreeMap;
  56. import org.eclipse.jgit.annotations.NonNull;
  57. import org.eclipse.jgit.errors.CommandFailedException;
  58. import org.eclipse.jgit.lfs.LfsPointer;
  59. import org.eclipse.jgit.lfs.Protocol;
  60. import org.eclipse.jgit.lfs.errors.LfsConfigInvalidException;
  61. import org.eclipse.jgit.lib.ConfigConstants;
  62. import org.eclipse.jgit.lib.Repository;
  63. import org.eclipse.jgit.lib.StoredConfig;
  64. import org.eclipse.jgit.transport.HttpConfig;
  65. import org.eclipse.jgit.transport.HttpTransport;
  66. import org.eclipse.jgit.transport.URIish;
  67. import org.eclipse.jgit.transport.http.HttpConnection;
  68. import org.eclipse.jgit.util.HttpSupport;
  69. import org.eclipse.jgit.util.SshSupport;
  70. /**
  71. * Provides means to get a valid LFS connection for a given repository.
  72. */
  73. public class LfsConnectionFactory {
  74. private static final int SSH_AUTH_TIMEOUT_SECONDS = 30;
  75. private static final String SCHEME_HTTPS = "https"; //$NON-NLS-1$
  76. private static final String SCHEME_SSH = "ssh"; //$NON-NLS-1$
  77. private static final Map<String, AuthCache> sshAuthCache = new TreeMap<>();
  78. /**
  79. * Determine URL of LFS server by looking into config parameters lfs.url,
  80. * lfs.[remote].url or remote.[remote].url. The LFS server URL is computed
  81. * from remote.[remote].url by appending "/info/lfs". In case there is no
  82. * URL configured, a SSH remote URI can be used to auto-detect the LFS URI
  83. * by using the remote "git-lfs-authenticate" command.
  84. *
  85. * @param db
  86. * the repository to work with
  87. * @param method
  88. * the method (GET,PUT,...) of the request this connection will
  89. * be used for
  90. * @param purpose
  91. * the action, e.g. Protocol.OPERATION_DOWNLOAD
  92. * @return the url for the lfs server. e.g.
  93. * "https://github.com/github/git-lfs.git/info/lfs"
  94. * @throws IOException
  95. */
  96. public static HttpConnection getLfsConnection(Repository db, String method,
  97. String purpose) throws IOException {
  98. StoredConfig config = db.getConfig();
  99. Map<String, String> additionalHeaders = new TreeMap<>();
  100. String lfsUrl = getLfsUrl(db, purpose, additionalHeaders);
  101. URL url = new URL(lfsUrl + Protocol.OBJECTS_LFS_ENDPOINT);
  102. HttpConnection connection = HttpTransport.getConnectionFactory().create(
  103. url, HttpSupport.proxyFor(ProxySelector.getDefault(), url));
  104. connection.setDoOutput(true);
  105. if (url.getProtocol().equals(SCHEME_HTTPS)
  106. && !config.getBoolean(HttpConfig.HTTP,
  107. HttpConfig.SSL_VERIFY_KEY, true)) {
  108. HttpSupport.disableSslVerify(connection);
  109. }
  110. connection.setRequestMethod(method);
  111. connection.setRequestProperty(HDR_ACCEPT,
  112. Protocol.CONTENTTYPE_VND_GIT_LFS_JSON);
  113. connection.setRequestProperty(HDR_CONTENT_TYPE,
  114. Protocol.CONTENTTYPE_VND_GIT_LFS_JSON);
  115. additionalHeaders
  116. .forEach((k, v) -> connection.setRequestProperty(k, v));
  117. return connection;
  118. }
  119. private static String getLfsUrl(Repository db, String purpose,
  120. Map<String, String> additionalHeaders)
  121. throws LfsConfigInvalidException {
  122. StoredConfig config = db.getConfig();
  123. String lfsUrl = config.getString(ConfigConstants.CONFIG_SECTION_LFS,
  124. null,
  125. ConfigConstants.CONFIG_KEY_URL);
  126. Exception ex = null;
  127. if (lfsUrl == null) {
  128. String remoteUrl = null;
  129. for (String remote : db.getRemoteNames()) {
  130. lfsUrl = config.getString(ConfigConstants.CONFIG_SECTION_LFS,
  131. remote,
  132. ConfigConstants.CONFIG_KEY_URL);
  133. // This could be done better (more precise logic), but according
  134. // to https://github.com/git-lfs/git-lfs/issues/1759 git-lfs
  135. // generally only supports 'origin' in an integrated workflow.
  136. if (lfsUrl == null && (remote.equals(
  137. org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME))) {
  138. remoteUrl = config.getString(
  139. ConfigConstants.CONFIG_KEY_REMOTE, remote,
  140. ConfigConstants.CONFIG_KEY_URL);
  141. }
  142. break;
  143. }
  144. if (lfsUrl == null && remoteUrl != null) {
  145. try {
  146. lfsUrl = discoverLfsUrl(db, purpose, additionalHeaders,
  147. remoteUrl);
  148. } catch (URISyntaxException | IOException
  149. | CommandFailedException e) {
  150. ex = e;
  151. }
  152. } else {
  153. lfsUrl = lfsUrl + Protocol.INFO_LFS_ENDPOINT;
  154. }
  155. }
  156. if (lfsUrl == null) {
  157. if (ex != null) {
  158. throw new LfsConfigInvalidException(
  159. LfsText.get().lfsNoDownloadUrl, ex);
  160. }
  161. throw new LfsConfigInvalidException(LfsText.get().lfsNoDownloadUrl);
  162. }
  163. return lfsUrl;
  164. }
  165. private static String discoverLfsUrl(Repository db, String purpose,
  166. Map<String, String> additionalHeaders, String remoteUrl)
  167. throws URISyntaxException, IOException, CommandFailedException {
  168. URIish u = new URIish(remoteUrl);
  169. if (u.getScheme() == null || SCHEME_SSH.equals(u.getScheme())) {
  170. Protocol.ExpiringAction action = getSshAuthentication(db, purpose,
  171. remoteUrl, u);
  172. additionalHeaders.putAll(action.header);
  173. return action.href;
  174. }
  175. return remoteUrl + Protocol.INFO_LFS_ENDPOINT;
  176. }
  177. private static Protocol.ExpiringAction getSshAuthentication(
  178. Repository db, String purpose, String remoteUrl, URIish u)
  179. throws IOException, CommandFailedException {
  180. AuthCache cached = sshAuthCache.get(remoteUrl);
  181. Protocol.ExpiringAction action = null;
  182. if (cached != null && cached.validUntil > System.currentTimeMillis()) {
  183. action = cached.cachedAction;
  184. }
  185. if (action == null) {
  186. // discover and authenticate; git-lfs does "ssh
  187. // -p <port> -- <host> git-lfs-authenticate
  188. // <project> <upload/download>"
  189. String json = SshSupport.runSshCommand(u.setPath(""), //$NON-NLS-1$
  190. null, db.getFS(),
  191. "git-lfs-authenticate " + extractProjectName(u) + " " //$NON-NLS-1$//$NON-NLS-2$
  192. + purpose,
  193. SSH_AUTH_TIMEOUT_SECONDS);
  194. action = Protocol.gson().fromJson(json,
  195. Protocol.ExpiringAction.class);
  196. // cache the result as long as possible.
  197. AuthCache c = new AuthCache(action);
  198. sshAuthCache.put(remoteUrl, c);
  199. }
  200. return action;
  201. }
  202. /**
  203. * Create a connection for the specified
  204. * {@link org.eclipse.jgit.lfs.Protocol.Action}.
  205. *
  206. * @param repo
  207. * the repo to fetch required configuration from
  208. * @param action
  209. * the action for which to create a connection
  210. * @param method
  211. * the target method (GET or PUT)
  212. * @return a connection. output mode is not set.
  213. * @throws IOException
  214. * in case of any error.
  215. */
  216. @NonNull
  217. public static HttpConnection getLfsContentConnection(
  218. Repository repo, Protocol.Action action, String method)
  219. throws IOException {
  220. URL contentUrl = new URL(action.href);
  221. HttpConnection contentServerConn = HttpTransport.getConnectionFactory()
  222. .create(contentUrl, HttpSupport
  223. .proxyFor(ProxySelector.getDefault(), contentUrl));
  224. contentServerConn.setRequestMethod(method);
  225. if (action.header != null) {
  226. action.header.forEach(
  227. (k, v) -> contentServerConn.setRequestProperty(k, v));
  228. }
  229. if (contentUrl.getProtocol().equals(SCHEME_HTTPS)
  230. && !repo.getConfig().getBoolean(HttpConfig.HTTP,
  231. HttpConfig.SSL_VERIFY_KEY, true)) {
  232. HttpSupport.disableSslVerify(contentServerConn);
  233. }
  234. contentServerConn.setRequestProperty(HDR_ACCEPT_ENCODING,
  235. ENCODING_GZIP);
  236. return contentServerConn;
  237. }
  238. private static String extractProjectName(URIish u) {
  239. String path = u.getPath();
  240. // begins with a slash if the url contains a port (gerrit vs. github).
  241. if (path.startsWith("/")) { //$NON-NLS-1$
  242. path = path.substring(1);
  243. }
  244. if (path.endsWith(org.eclipse.jgit.lib.Constants.DOT_GIT)) {
  245. return path.substring(0, path.length() - 4);
  246. }
  247. return path;
  248. }
  249. /**
  250. * @param operation
  251. * the operation to perform, e.g. Protocol.OPERATION_DOWNLOAD
  252. * @param resources
  253. * the LFS resources affected
  254. * @return a request that can be serialized to JSON
  255. */
  256. public static Protocol.Request toRequest(String operation,
  257. LfsPointer... resources) {
  258. Protocol.Request req = new Protocol.Request();
  259. req.operation = operation;
  260. if (resources != null) {
  261. req.objects = new LinkedList<>();
  262. for (LfsPointer res : resources) {
  263. Protocol.ObjectSpec o = new Protocol.ObjectSpec();
  264. o.oid = res.getOid().getName();
  265. o.size = res.getSize();
  266. req.objects.add(o);
  267. }
  268. }
  269. return req;
  270. }
  271. private static final class AuthCache {
  272. private static final long AUTH_CACHE_EAGER_TIMEOUT = 500;
  273. private static final SimpleDateFormat ISO_FORMAT = new SimpleDateFormat(
  274. "yyyy-MM-dd'T'HH:mm:ss.SSSX"); //$NON-NLS-1$
  275. /**
  276. * Creates a cache entry for an authentication response.
  277. * <p>
  278. * The timeout of the cache token is extracted from the given action. If
  279. * no timeout can be determined, the token will be used only once.
  280. *
  281. * @param action
  282. */
  283. public AuthCache(Protocol.ExpiringAction action) {
  284. this.cachedAction = action;
  285. try {
  286. if (action.expiresIn != null && !action.expiresIn.isEmpty()) {
  287. this.validUntil = (System.currentTimeMillis()
  288. + Long.parseLong(action.expiresIn))
  289. - AUTH_CACHE_EAGER_TIMEOUT;
  290. } else if (action.expiresAt != null
  291. && !action.expiresAt.isEmpty()) {
  292. this.validUntil = ISO_FORMAT.parse(action.expiresAt)
  293. .getTime() - AUTH_CACHE_EAGER_TIMEOUT;
  294. } else {
  295. this.validUntil = System.currentTimeMillis();
  296. }
  297. } catch (Exception e) {
  298. this.validUntil = System.currentTimeMillis();
  299. }
  300. }
  301. long validUntil;
  302. Protocol.ExpiringAction cachedAction;
  303. }
  304. }