/* * Copyright 2014-2017 Andrew Gaul <andrew@gaul.org> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.gaul.s3proxy; import static java.util.Objects.requireNonNull; import static com.google.common.base.Preconditions.checkArgument; import java.net.URI; import com.google.common.base.Optional; import com.google.common.base.Strings; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.jclouds.blobstore.BlobStore; /** * S3Proxy translates S3 HTTP operations into jclouds provider-agnostic * operations. This allows applications using the S3 API to interface with any * provider that jclouds supports, e.g., EMC Atmos, Microsoft Azure, * OpenStack Swift. */ public final class S3Proxy { private final Server server; private final S3ProxyHandlerJetty handler; private final boolean listenHTTP; private final boolean listenHTTPS; static { // Prevent Jetty from rewriting headers: // https://bugs.eclipse.org/bugs/show_bug.cgi?id=414449 System.setProperty("org.eclipse.jetty.http.HttpParser.STRICT", "true"); } S3Proxy(Builder builder) { checkArgument(builder.endpoint != null || builder.secureEndpoint != null, "Must provide endpoint or secure-endpoint"); if (builder.endpoint != null) { checkArgument(builder.endpoint.getPath().isEmpty(), "endpoint path must be empty, was: %s", builder.endpoint.getPath()); } if (builder.secureEndpoint != null) { checkArgument(builder.secureEndpoint.getPath().isEmpty(), "secure-endpoint path must be empty, was: %s", builder.secureEndpoint.getPath()); requireNonNull(builder.keyStorePath, "Must provide keyStorePath with HTTPS endpoint"); requireNonNull(builder.keyStorePassword, "Must provide keyStorePassword with HTTPS endpoint"); } checkArgument(Strings.isNullOrEmpty(builder.identity) ^ !Strings.isNullOrEmpty(builder.credential), "Must provide both identity and credential"); server = new Server(); if (builder.servicePath != null && !builder.servicePath.isEmpty()) { ContextHandler context = new ContextHandler(); context.setContextPath(builder.servicePath); } ((QueuedThreadPool) server.getThreadPool()).setName("S3Proxy"); HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(); ServerConnector connector; if (builder.endpoint != null) { connector = new ServerConnector(server, httpConnectionFactory); connector.setHost(builder.endpoint.getHost()); connector.setPort(builder.endpoint.getPort()); server.addConnector(connector); listenHTTP = true; } else { listenHTTP = false; } if (builder.secureEndpoint != null) { SslContextFactory sslContextFactory = new SslContextFactory(); sslContextFactory.setKeyStorePath(builder.keyStorePath); sslContextFactory.setKeyStorePassword(builder.keyStorePassword); connector = new ServerConnector(server, sslContextFactory, httpConnectionFactory); connector.setHost(builder.secureEndpoint.getHost()); connector.setPort(builder.secureEndpoint.getPort()); server.addConnector(connector); listenHTTPS = true; } else { listenHTTPS = false; } handler = new S3ProxyHandlerJetty(builder.blobStore, builder.authenticationType, builder.identity, builder.credential, Optional.fromNullable(builder.virtualHost), builder.v4MaxNonChunkedRequestSize, builder.ignoreUnknownHeaders, builder.corsAllowAll, builder.servicePath); server.setHandler(handler); } public static final class Builder { private BlobStore blobStore; private URI endpoint; private URI secureEndpoint; private AuthenticationType authenticationType = AuthenticationType.NONE; private String identity; private String credential; private String keyStorePath; private String keyStorePassword; private String virtualHost; private String servicePath; private long v4MaxNonChunkedRequestSize = 32 * 1024 * 1024; private boolean ignoreUnknownHeaders; private boolean corsAllowAll; Builder() { } public S3Proxy build() { return new S3Proxy(this); } public Builder blobStore(BlobStore blobStore) { this.blobStore = requireNonNull(blobStore); return this; } public Builder endpoint(URI endpoint) { this.endpoint = requireNonNull(endpoint); return this; } public Builder secureEndpoint(URI secureEndpoint) { this.secureEndpoint = requireNonNull(secureEndpoint); return this; } public Builder awsAuthentication(AuthenticationType authenticationType, String identity, String credential) { this.authenticationType = authenticationType; this.identity = requireNonNull(identity); this.credential = requireNonNull(credential); return this; } public Builder keyStore(String keyStorePath, String keyStorePassword) { this.keyStorePath = requireNonNull(keyStorePath); this.keyStorePassword = requireNonNull(keyStorePassword); return this; } public Builder virtualHost(String virtualHost) { this.virtualHost = requireNonNull(virtualHost); return this; } public Builder v4MaxNonChunkedRequestSize( long v4MaxNonChunkedRequestSize) { if (v4MaxNonChunkedRequestSize <= 0) { throw new IllegalArgumentException( "must be greater than zero, was: " + v4MaxNonChunkedRequestSize); } this.v4MaxNonChunkedRequestSize = v4MaxNonChunkedRequestSize; return this; } public Builder ignoreUnknownHeaders(boolean ignoreUnknownHeaders) { this.ignoreUnknownHeaders = ignoreUnknownHeaders; return this; } public Builder corsAllowAll(boolean corsAllowAll) { this.corsAllowAll = corsAllowAll; return this; } public void servicePath(String s3ProxyServicePath) { String path = Strings.nullToEmpty(s3ProxyServicePath); if (!path.isEmpty()) { if (!path.startsWith("/")) { path = "/" + path; } } this.servicePath = path; } } public static Builder builder() { return new Builder(); } public void start() throws Exception { server.start(); } public void stop() throws Exception { server.stop(); } public int getPort() { if (listenHTTP) { return ((ServerConnector) server.getConnectors()[0]).getLocalPort(); } else { return -1; } } public int getSecurePort() { if (listenHTTPS) { ServerConnector connector; if (listenHTTP) { connector = (ServerConnector) server.getConnectors()[1]; } else { connector = (ServerConnector) server.getConnectors()[0]; } return connector.getLocalPort(); } return -1; } public String getState() { return server.getState(); } public void setBlobStoreLocator(BlobStoreLocator lookup) { handler.getHandler().setBlobStoreLocator(lookup); } }