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  	@Override
95  	public Response.Action getDownloadAction(AnyLongObjectId oid) {
96  		URL endpointUrl = getObjectUrl(oid);
97  		Map<String, String> queryParams = new HashMap<>();
98  		queryParams.put(X_AMZ_EXPIRES,
99  				Integer.toString(s3Config.getExpirationSeconds()));
100 		Map<String, String> headers = new HashMap<>();
101 		String authorizationQueryParameters = SignerV4.createAuthorizationQuery(
102 				s3Config, endpointUrl, METHOD_GET, headers, queryParams,
103 				UNSIGNED_PAYLOAD);
104 
105 		Response.Action a = new Response.Action();
106 		a.href = endpointUrl.toString() + "?" + authorizationQueryParameters; //$NON-NLS-1$
107 		return a;
108 	}
109 
110 	@Override
111 	public Response.Action getUploadAction(AnyLongObjectId oid, long size) {
112 		cacheObjectMetaData(oid, size);
113 		URL objectUrl = getObjectUrl(oid);
114 		Map<String, String> headers = new HashMap<>();
115 		headers.put(X_AMZ_CONTENT_SHA256, oid.getName());
116 		headers.put(HDR_CONTENT_LENGTH, Long.toString(size));
117 		headers.put(X_AMZ_STORAGE_CLASS, s3Config.getStorageClass());
118 		headers.put(HttpSupport.HDR_CONTENT_TYPE, "application/octet-stream"); //$NON-NLS-1$
119 		headers = SignerV4.createHeaderAuthorization(s3Config, objectUrl,
120 				METHOD_PUT, headers, oid.getName());
121 
122 		Response.Action a = new Response.Action();
123 		a.href = objectUrl.toString();
124 		a.header = new HashMap<>();
125 		a.header.putAll(headers);
126 		return a;
127 	}
128 
129 	@Override
130 	public Action getVerifyAction(AnyLongObjectId id) {
131 		return null; // TODO(ms) implement this
132 	}
133 
134 	@Override
135 	public long getSize(AnyLongObjectId oid) throws IOException {
136 		URL endpointUrl = getObjectUrl(oid);
137 		Map<String, String> queryParams = new HashMap<>();
138 		queryParams.put(X_AMZ_EXPIRES,
139 				Integer.toString(s3Config.getExpirationSeconds()));
140 		Map<String, String> headers = new HashMap<>();
141 
142 		String authorizationQueryParameters = SignerV4.createAuthorizationQuery(
143 				s3Config, endpointUrl, METHOD_HEAD, headers, queryParams,
144 				UNSIGNED_PAYLOAD);
145 		String href = endpointUrl.toString() + "?" //$NON-NLS-1$
146 				+ authorizationQueryParameters;
147 
148 		Proxy proxy = HttpSupport.proxyFor(ProxySelector.getDefault(),
149 				endpointUrl);
150 		HttpClientConnectionFactory f = new HttpClientConnectionFactory();
151 		HttpConnection conn = f.create(new URL(href), proxy);
152 		if (s3Config.isDisableSslVerify()) {
153 			HttpSupport.disableSslVerify(conn);
154 		}
155 		conn.setRequestMethod(METHOD_HEAD);
156 		conn.connect();
157 		int status = conn.getResponseCode();
158 		if (status == SC_OK) {
159 			String contentLengthHeader = conn
160 					.getHeaderField(HDR_CONTENT_LENGTH);
161 			if (contentLengthHeader != null) {
162 				return Integer.parseInt(contentLengthHeader);
163 			}
164 		}
165 		return -1;
166 	}
167 
168 	/**
169 	 * Cache metadata (size) for an object to avoid extra roundtrip to S3 in
170 	 * order to retrieve this metadata for a given object. Subclasses can
171 	 * implement a local cache and override {{@link #getSize(AnyLongObjectId)}
172 	 * to retrieve the object size from the local cache to eliminate the need
173 	 * for another roundtrip to S3
174 	 *
175 	 * @param oid
176 	 *            the object id identifying the object to be cached
177 	 * @param size
178 	 *            the object's size (in bytes)
179 	 */
180 	protected void cacheObjectMetaData(AnyLongObjectId oid, long size) {
181 		// no caching
182 	}
183 
184 	private void validateConfig(S3Config config) {
185 		assertNotEmpty(LfsServerText.get().undefinedS3AccessKey,
186 				config.getAccessKey());
187 		assertNotEmpty(LfsServerText.get().undefinedS3Bucket,
188 				config.getBucket());
189 		assertNotEmpty(LfsServerText.get().undefinedS3Region,
190 				config.getRegion());
191 		assertNotEmpty(LfsServerText.get().undefinedS3SecretKey,
192 				config.getSecretKey());
193 		assertNotEmpty(LfsServerText.get().undefinedS3StorageClass,
194 				config.getStorageClass());
195 	}
196 
197 	private void assertNotEmpty(String message, String value) {
198 		if (value == null || value.trim().length() == 0) {
199 			throw new IllegalArgumentException(message);
200 		}
201 	}
202 
203 	private URL getObjectUrl(AnyLongObjectId oid) {
204 		try {
205 			return new URL(String.format("https://s3-%s.amazonaws.com/%s/%s", //$NON-NLS-1$
206 					s3Config.getRegion(), s3Config.getBucket(),
207 					getPath(oid)));
208 		} catch (MalformedURLException e) {
209 			throw new IllegalArgumentException(MessageFormat.format(
210 					LfsServerText.get().unparsableEndpoint, e.getMessage()));
211 		}
212 	}
213 
214 	private String getPath(AnyLongObjectId oid) {
215 		return oid.getName();
216 	}
217 }