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 package com.s3auth.hosts;
31
32 import com.amazonaws.AmazonClientException;
33 import com.amazonaws.AmazonServiceException;
34 import com.amazonaws.ClientConfiguration;
35 import com.amazonaws.Protocol;
36 import com.amazonaws.auth.BasicAWSCredentials;
37 import com.amazonaws.services.cloudwatch.AmazonCloudWatchAsyncClient;
38 import com.amazonaws.services.cloudwatch.AmazonCloudWatchClient;
39 import com.amazonaws.services.cloudwatch.model.Datapoint;
40 import com.amazonaws.services.cloudwatch.model.Dimension;
41 import com.amazonaws.services.cloudwatch.model.GetMetricStatisticsRequest;
42 import com.amazonaws.services.cloudwatch.model.StandardUnit;
43 import com.amazonaws.services.s3.model.BucketWebsiteConfiguration;
44 import com.jcabi.aspects.Cacheable;
45 import com.jcabi.aspects.Immutable;
46 import com.jcabi.aspects.Loggable;
47 import com.jcabi.aspects.Tv;
48 import com.jcabi.log.Logger;
49 import com.jcabi.manifests.Manifests;
50 import java.io.IOException;
51 import java.net.URI;
52 import java.util.Collection;
53 import java.util.Date;
54 import java.util.LinkedList;
55 import java.util.List;
56 import java.util.concurrent.Executors;
57 import java.util.concurrent.TimeUnit;
58 import javax.validation.constraints.NotNull;
59 import lombok.EqualsAndHashCode;
60 import org.apache.commons.lang3.StringUtils;
61 import org.apache.commons.lang3.time.DateUtils;
62 import org.apache.http.HttpStatus;
63
64
65
66
67
68
69
70
71
72 @Immutable
73 @EqualsAndHashCode(of = "bucket")
74 @Loggable(Loggable.DEBUG)
75 @SuppressWarnings({ "PMD.CyclomaticComplexity", "PMD.ExcessiveImports" })
76 final class DefaultHost implements Host {
77
78
79
80
81 private static final String SUFFIX = "index.html";
82
83
84
85
86 private static final Host.CloudWatch CLOUDWATCH = new Host.CloudWatch() {
87 @Override
88 @Cacheable(lifetime = 1, unit = TimeUnit.HOURS)
89 public AmazonCloudWatchClient get() {
90 return new AmazonCloudWatchAsyncClient(
91 new BasicAWSCredentials(
92 Manifests.read("S3Auth-AwsCloudWatchKey"),
93 Manifests.read("S3Auth-AwsCloudWatchSecret")
94 ),
95 new ClientConfiguration().withProtocol(Protocol.HTTP),
96 Executors.newFixedThreadPool(Tv.FIFTY)
97 );
98 }
99 };
100
101
102
103
104 private final transient Bucket bucket;
105
106
107
108
109 private final transient Htpasswd htpasswd;
110
111
112
113
114 private final transient Stats statistics;
115
116
117
118
119 private final transient Host.CloudWatch cloudwatch;
120
121
122
123
124
125 DefaultHost(@NotNull final Bucket bckt) {
126 this(
127 bckt,
128 DefaultHost.CLOUDWATCH
129 );
130 }
131
132
133
134
135
136
137 DefaultHost(
138 @NotNull final Bucket bckt,
139 @NotNull final Host.CloudWatch cwatch
140 ) {
141 this.bucket = bckt;
142 this.htpasswd = new Htpasswd(this);
143 this.cloudwatch = cwatch;
144 this.statistics = new HostStats(this.bucket.bucket());
145 }
146
147 @Override
148 public String toString() {
149 return this.bucket.toString();
150 }
151
152 @Override
153 public void close() throws IOException {
154
155 }
156
157
158
159 @Override
160 @NotNull
161 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
162 @Loggable(value = Loggable.DEBUG, ignore = IOException.class)
163 public Resource fetch(@NotNull final URI uri,
164 @NotNull final Range range, @NotNull final Version version)
165 throws IOException {
166 Resource resource = null;
167 final Collection<String> errors = new LinkedList<String>();
168 final DomainStatsData data = new H2DomainStatsData();
169 for (final DefaultHost.ObjectName name : this.names(uri)) {
170 try {
171 if (version.list()) {
172 resource = new ObjectVersionListing(
173 this.bucket.client(), this.bucket.bucket(), name.get()
174 );
175 } else {
176 resource = new DefaultResource(
177 this.bucket.client(), this.bucket.bucket(),
178 name.get(), range, version, data
179 );
180 }
181 break;
182 } catch (final AmazonServiceException ex) {
183 if (StringUtils.endsWith(name.get(), DefaultHost.SUFFIX)
184 && "NoSuchKey".equals(ex.getErrorCode())
185 ) {
186 resource = new DirectoryListing(
187 this.bucket.client(), this.bucket.bucket(),
188 StringUtils.removeEnd(name.get(), DefaultHost.SUFFIX)
189 );
190 break;
191 } else if ("NoSuchBucket".equals(ex.getErrorCode())) {
192 throw new IOException(
193 Logger.format(
194 "The bucket '%s' does not exist.",
195 this.bucket.bucket()
196 ),
197 ex
198 );
199 } else if (ex.getStatusCode() >= HttpStatus.SC_BAD_REQUEST
200 && ex.getStatusCode() < HttpStatus.SC_INTERNAL_SERVER_ERROR
201 ) {
202 try {
203 final BucketWebsiteConfiguration config =
204 this.bucket.client().getBucketWebsiteConfiguration(
205 this.bucket.bucket()
206 );
207 if (config != null
208 && config.getErrorDocument() != null) {
209 resource = new DefaultResource(
210 this.bucket.client(), this.bucket.bucket(),
211 config.getErrorDocument(), Range.ENTIRE,
212 Version.LATEST, data
213 );
214 }
215 } catch (final AmazonClientException exc) {
216
217 errors.add(
218 String.format("'%s': %s", name, exc.getMessage())
219 );
220 }
221 }
222 errors.add(String.format("'%s': %s", name, ex.getMessage()));
223 } catch (final AmazonClientException ex) {
224 errors.add(String.format("'%s': %s", name, ex.getMessage()));
225 }
226 }
227 if (resource == null) {
228 throw new IOException(
229 Logger.format(
230 "failed to fetch %s from '%s' (key=%s): %[list]s",
231 uri, this.bucket.name(), this.bucket.key(), errors
232 )
233 );
234 }
235 return resource;
236 }
237
238 @Override
239 public boolean isHidden(@NotNull final URI uri) {
240 return true;
241 }
242
243 @Override
244 public boolean authorized(@NotNull final String user,
245 @NotNull final String password) throws IOException {
246 final boolean auth;
247 if (user.equals(this.bucket.key())
248 && password.equals(this.bucket.secret())) {
249 auth = true;
250 } else {
251 auth = this.htpasswd.authorized(user, password);
252 }
253 return auth;
254 }
255
256 @Override
257 public String syslog() {
258 return this.bucket.syslog();
259 }
260
261 @Override
262 public Stats stats() {
263 return this.statistics;
264 }
265
266
267
268
269
270
271 private Iterable<DefaultHost.ObjectName> names(final URI uri) {
272 final String name = StringUtils.strip(uri.getPath(), "/");
273 final Collection<DefaultHost.ObjectName> names =
274 new LinkedList<DefaultHost.ObjectName>();
275 if (!name.isEmpty()) {
276 names.add(new DefaultHost.Simple(name));
277 }
278 names.add(new DefaultHost.NameWithSuffix(name));
279 return names;
280 }
281
282
283
284
285 @Loggable(Loggable.DEBUG)
286 private final class NameWithSuffix implements DefaultHost.ObjectName {
287
288
289
290 private final transient String origin;
291
292
293
294
295 NameWithSuffix(final String name) {
296 this.origin = name;
297 }
298 @Override
299 public String get() {
300 String suffix = null;
301 try {
302 final BucketWebsiteConfiguration conf =
303 DefaultHost.this.bucket.client()
304 .getBucketWebsiteConfiguration(
305 DefaultHost.this.bucket.name()
306 );
307 if (conf != null) {
308 suffix = conf.getIndexDocumentSuffix();
309 }
310 } catch (final AmazonClientException ex) {
311 suffix = "";
312 }
313 if (suffix == null || suffix.isEmpty()) {
314 suffix = DefaultHost.SUFFIX;
315 }
316 final StringBuilder text = new StringBuilder(this.origin);
317 if (text.length() > 0) {
318 text.append('/');
319 }
320 text.append(suffix);
321 return text.toString();
322 }
323 @Override
324 public String toString() {
325 return String.format("%s+suffix", this.origin);
326 }
327 }
328
329
330
331
332 @EqualsAndHashCode(of = "name")
333 private static final class Simple implements DefaultHost.ObjectName {
334
335
336
337 private final transient String name;
338
339
340
341
342 Simple(final String nme) {
343 this.name = nme;
344 }
345 @Override
346 public String get() {
347 return this.name;
348 }
349 @Override
350 public String toString() {
351 return this.name;
352 }
353 }
354
355
356
357
358 @Loggable(Loggable.DEBUG)
359 @EqualsAndHashCode(of = "bucket")
360 private final class HostStats implements Stats {
361
362
363
364 private final transient String bucket;
365
366
367
368
369 public HostStats(final String bckt) {
370 this.bucket = bckt;
371 }
372 @Override
373 @Cacheable(lifetime = Tv.THIRTY, unit = TimeUnit.MINUTES)
374 public long bytesTransferred() {
375 final Date now = new Date();
376 final List<Datapoint> datapoints =
377 DefaultHost.this.cloudwatch.get().getMetricStatistics(
378 new GetMetricStatisticsRequest()
379 .withMetricName("BytesTransferred")
380 .withNamespace("S3Auth")
381 .withStatistics("Sum")
382 .withDimensions(
383 new Dimension()
384 .withName("Bucket")
385 .withValue(this.bucket)
386 )
387 .withUnit(StandardUnit.Bytes)
388 .withPeriod((int) TimeUnit.DAYS.toSeconds(Tv.SEVEN))
389 .withStartTime(DateUtils.addWeeks(now, -1))
390 .withEndTime(now)
391 ).getDatapoints();
392 long sum = 0L;
393 for (final Datapoint datapoint : datapoints) {
394 sum += datapoint.getSum();
395 }
396 return sum;
397 }
398 }
399
400
401
402
403 private interface ObjectName {
404
405
406
407
408 String get();
409 }
410
411 }