View Javadoc
1   /*
2    * Copyright (C) 2015, Matthias Sohn <matthias.sohn@sap.com>
3    * Copyright (C) 2015, Sasa Zivkov <sasa.zivkov@sap.com>
4    * and other copyright owners as documented in the project's IP log.
5    *
6    * This program and the accompanying materials are made available
7    * under the terms of the Eclipse Distribution License v1.0 which
8    * accompanies this distribution, is reproduced below, and is
9    * available at http://www.eclipse.org/org/documents/edl-v10.php
10   *
11   * All rights reserved.
12   *
13   * Redistribution and use in source and binary forms, with or
14   * without modification, are permitted provided that the following
15   * conditions are met:
16   *
17   * - Redistributions of source code must retain the above copyright
18   *   notice, this list of conditions and the following disclaimer.
19   *
20   * - Redistributions in binary form must reproduce the above
21   *   copyright notice, this list of conditions and the following
22   *   disclaimer in the documentation and/or other materials provided
23   *   with the distribution.
24   *
25   * - Neither the name of the Eclipse Foundation, Inc. nor the
26   *   names of its contributors may be used to endorse or promote
27   *   products derived from this software without specific prior
28   *   written permission.
29   *
30   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
31   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
32   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
33   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
34   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
35   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
36   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
37   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
38   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
39   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
40   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
41   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
42   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
43   */
44  package org.eclipse.jgit.lfs.server.s3;
45  
46  import static javax.servlet.http.HttpServletResponse.SC_OK;
47  import static org.eclipse.jgit.lfs.server.s3.SignerV4.UNSIGNED_PAYLOAD;
48  import static org.eclipse.jgit.lfs.server.s3.SignerV4.X_AMZ_CONTENT_SHA256;
49  import static org.eclipse.jgit.lfs.server.s3.SignerV4.X_AMZ_EXPIRES;
50  import static org.eclipse.jgit.lfs.server.s3.SignerV4.X_AMZ_STORAGE_CLASS;
51  import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_LENGTH;
52  import static org.eclipse.jgit.util.HttpSupport.METHOD_GET;
53  import static org.eclipse.jgit.util.HttpSupport.METHOD_HEAD;
54  import static org.eclipse.jgit.util.HttpSupport.METHOD_PUT;
55  
56  import java.io.IOException;
57  import java.net.MalformedURLException;
58  import java.net.Proxy;
59  import java.net.ProxySelector;
60  import java.net.URL;
61  import java.text.MessageFormat;
62  import java.util.HashMap;
63  import java.util.Map;
64  
65  import org.eclipse.jgit.lfs.lib.AnyLongObjectId;
66  import org.eclipse.jgit.lfs.server.LargeFileRepository;
67  import org.eclipse.jgit.lfs.server.Response;
68  import org.eclipse.jgit.lfs.server.Response.Action;
69  import org.eclipse.jgit.lfs.server.internal.LfsServerText;
70  import org.eclipse.jgit.transport.http.HttpConnection;
71  import org.eclipse.jgit.transport.http.apache.HttpClientConnectionFactory;
72  import org.eclipse.jgit.util.HttpSupport;
73  
74  /**
75   * Repository storing LFS objects in Amazon S3
76   *
77   * @since 4.3
78   */
79  public class S3Repository implements LargeFileRepository {
80  
81  	private S3Config s3Config;
82  
83  	/**
84  	 * Construct a LFS repository storing large objects in Amazon S3
85  	 *
86  	 * @param config
87  	 *            AWS S3 storage bucket configuration
88  	 */
89  	public S3Repository(S3Config config) {
90  		validateConfig(config);
91  		this.s3Config = config;
92  	}
93  
94  	/** {@inheritDoc} */
95  	@Override
96  	public Response.Action getDownloadAction(AnyLongObjectId oid) {
97  		URL endpointUrl = getObjectUrl(oid);
98  		Map<String, String> queryParams = new HashMap<>();
99  		queryParams.put(X_AMZ_EXPIRES,
100 				Integer.toString(s3Config.getExpirationSeconds()));
101 		Map<String, String> headers = new HashMap<>();
102 		String authorizationQueryParameters = SignerV4.createAuthorizationQuery(
103 				s3Config, endpointUrl, METHOD_GET, headers, queryParams,
104 				UNSIGNED_PAYLOAD);
105 
106 		Response.Action a = new Response.Action();
107 		a.href = endpointUrl.toString() + "?" + authorizationQueryParameters; //$NON-NLS-1$
108 		return a;
109 	}
110 
111 	/** {@inheritDoc} */
112 	@Override
113 	public Response.Action getUploadAction(AnyLongObjectId oid, long size) {
114 		cacheObjectMetaData(oid, size);
115 		URL objectUrl = getObjectUrl(oid);
116 		Map<String, String> headers = new HashMap<>();
117 		headers.put(X_AMZ_CONTENT_SHA256, oid.getName());
118 		headers.put(HDR_CONTENT_LENGTH, Long.toString(size));
119 		headers.put(X_AMZ_STORAGE_CLASS, s3Config.getStorageClass());
120 		headers.put(HttpSupport.HDR_CONTENT_TYPE, "application/octet-stream"); //$NON-NLS-1$
121 		headers = SignerV4.createHeaderAuthorization(s3Config, objectUrl,
122 				METHOD_PUT, headers, oid.getName());
123 
124 		Response.Action a = new Response.Action();
125 		a.href = objectUrl.toString();
126 		a.header = new HashMap<>();
127 		a.header.putAll(headers);
128 		return a;
129 	}
130 
131 	/** {@inheritDoc} */
132 	@Override
133 	public Action getVerifyAction(AnyLongObjectId id) {
134 		return null; // TODO(ms) implement this
135 	}
136 
137 	/** {@inheritDoc} */
138 	@Override
139 	public long getSize(AnyLongObjectId oid) throws IOException {
140 		URL endpointUrl = getObjectUrl(oid);
141 		Map<String, String> queryParams = new HashMap<>();
142 		queryParams.put(X_AMZ_EXPIRES,
143 				Integer.toString(s3Config.getExpirationSeconds()));
144 		Map<String, String> headers = new HashMap<>();
145 
146 		String authorizationQueryParameters = SignerV4.createAuthorizationQuery(
147 				s3Config, endpointUrl, METHOD_HEAD, headers, queryParams,
148 				UNSIGNED_PAYLOAD);
149 		String href = endpointUrl.toString() + "?" //$NON-NLS-1$
150 				+ authorizationQueryParameters;
151 
152 		Proxy proxy = HttpSupport.proxyFor(ProxySelector.getDefault(),
153 				endpointUrl);
154 		HttpClientConnectionFactory f = new HttpClientConnectionFactory();
155 		HttpConnection conn = f.create(new URL(href), proxy);
156 		if (s3Config.isDisableSslVerify()) {
157 			HttpSupport.disableSslVerify(conn);
158 		}
159 		conn.setRequestMethod(METHOD_HEAD);
160 		conn.connect();
161 		int status = conn.getResponseCode();
162 		if (status == SC_OK) {
163 			String contentLengthHeader = conn
164 					.getHeaderField(HDR_CONTENT_LENGTH);
165 			if (contentLengthHeader != null) {
166 				return Integer.parseInt(contentLengthHeader);
167 			}
168 		}
169 		return -1;
170 	}
171 
172 	/**
173 	 * Cache metadata (size) for an object to avoid extra roundtrip to S3 in
174 	 * order to retrieve this metadata for a given object. Subclasses can
175 	 * implement a local cache and override {{@link #getSize(AnyLongObjectId)}
176 	 * to retrieve the object size from the local cache to eliminate the need
177 	 * for another roundtrip to S3
178 	 *
179 	 * @param oid
180 	 *            the object id identifying the object to be cached
181 	 * @param size
182 	 *            the object's size (in bytes)
183 	 */
184 	protected void cacheObjectMetaData(AnyLongObjectId oid, long size) {
185 		// no caching
186 	}
187 
188 	private void validateConfig(S3Config config) {
189 		assertNotEmpty(LfsServerText.get().undefinedS3AccessKey,
190 				config.getAccessKey());
191 		assertNotEmpty(LfsServerText.get().undefinedS3Bucket,
192 				config.getBucket());
193 		assertNotEmpty(LfsServerText.get().undefinedS3Region,
194 				config.getRegion());
195 		assertNotEmpty(LfsServerText.get().undefinedS3SecretKey,
196 				config.getSecretKey());
197 		assertNotEmpty(LfsServerText.get().undefinedS3StorageClass,
198 				config.getStorageClass());
199 	}
200 
201 	private void assertNotEmpty(String message, String value) {
202 		if (value == null || value.trim().length() == 0) {
203 			throw new IllegalArgumentException(message);
204 		}
205 	}
206 
207 	private URL getObjectUrl(AnyLongObjectId oid) {
208 		try {
209 			return new URL(String.format("https://s3-%s.amazonaws.com/%s/%s", //$NON-NLS-1$
210 					s3Config.getRegion(), s3Config.getBucket(),
211 					getPath(oid)));
212 		} catch (MalformedURLException e) {
213 			throw new IllegalArgumentException(MessageFormat.format(
214 					LfsServerText.get().unparsableEndpoint, e.getMessage()));
215 		}
216 	}
217 
218 	private String getPath(AnyLongObjectId oid) {
219 		return oid.getName();
220 	}
221 }