From 69611feaa62e126666d60052cf56cd764bdcd339 Mon Sep 17 00:00:00 2001 From: "Bala.FA" Date: Mon, 10 Jul 2023 20:55:05 +0530 Subject: [PATCH] Add generic AWS S3 domain support Signed-off-by: Bala.FA --- api/src/main/java/io/minio/BucketArgs.java | 38 +-- .../main/java/io/minio/MinioAsyncClient.java | 148 +++++------ api/src/main/java/io/minio/MinioClient.java | 19 +- api/src/main/java/io/minio/S3Base.java | 236 ++++++++++++------ .../main/java/io/minio/http/HttpUtils.java | 49 ++-- 5 files changed, 293 insertions(+), 197 deletions(-) diff --git a/api/src/main/java/io/minio/BucketArgs.java b/api/src/main/java/io/minio/BucketArgs.java index 63ae0a420..a7877eb10 100644 --- a/api/src/main/java/io/minio/BucketArgs.java +++ b/api/src/main/java/io/minio/BucketArgs.java @@ -16,7 +16,10 @@ package io.minio; +import io.minio.http.HttpUtils; +import io.minio.org.apache.commons.validator.routines.InetAddressValidator; import java.util.Objects; +import java.util.regex.Pattern; /** Base argument class holds bucket name and region. */ public abstract class BucketArgs extends BaseArgs { @@ -34,32 +37,35 @@ public String region() { /** Base argument builder class for {@link BucketArgs}. */ public abstract static class Builder, A extends BucketArgs> extends BaseArgs.Builder { + private static final Pattern BUCKET_NAME_REGEX = + Pattern.compile("^[a-z0-9][a-z0-9\\.\\-]{1,61}[a-z0-9]$"); + protected void validateBucketName(String name) { validateNotNull(name, "bucket name"); - // Bucket names cannot be no less than 3 and no more than 63 characters long. - if (name.length() < 3 || name.length() > 63) { + if (!BUCKET_NAME_REGEX.matcher(name).find()) { throw new IllegalArgumentException( - name + " : " + "bucket name must be at least 3 and no more than 63 characters long"); + "bucket name '" + + name + + "' does not follow Amazon S3 standards. For more information refer " + + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html"); } - // Successive periods in bucket names are not allowed. - if (name.contains("..")) { - String msg = - "bucket name cannot contain successive periods. For more information refer " - + "http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html"; - throw new IllegalArgumentException(name + " : " + msg); + + if (InetAddressValidator.getInstance().isValidInet4Address(name)) { + throw new IllegalArgumentException( + "bucket name '" + name + "' must not be formatted as an IP address"); } - // Bucket names should be dns compatible. - if (!name.matches("^[a-z0-9][a-z0-9\\.\\-]+[a-z0-9]$")) { - String msg = - "bucket name does not follow Amazon S3 standards. For more information refer " - + "http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html"; - throw new IllegalArgumentException(name + " : " + msg); + + if (name.contains("..") || name.contains(".-") || name.contains("-.")) { + throw new IllegalArgumentException( + "bucket name '" + name + "' cannot contain successive characters '..', '.-' and '-.'"); } } private void validateRegion(String region) { - validateNullOrNotEmptyString(region, "region"); + if (region != null && !HttpUtils.REGION_REGEX.matcher(region).find()) { + throw new IllegalArgumentException("invalid region " + region); + } } @Override diff --git a/api/src/main/java/io/minio/MinioAsyncClient.java b/api/src/main/java/io/minio/MinioAsyncClient.java index 8c028a740..405ecd589 100644 --- a/api/src/main/java/io/minio/MinioAsyncClient.java +++ b/api/src/main/java/io/minio/MinioAsyncClient.java @@ -66,6 +66,7 @@ import java.nio.file.StandardOpenOption; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.util.Arrays; import java.util.Date; import java.util.Iterator; import java.util.LinkedList; @@ -75,6 +76,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; +import java.util.regex.Matcher; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -131,22 +133,20 @@ public class MinioAsyncClient extends S3Base { private MinioAsyncClient( HttpUrl baseUrl, - String region, - boolean isAwsHost, - boolean isFipsHost, - boolean isAccelerateHost, - boolean isDualStackHost, + String awsS3Prefix, + String awsDomainSuffix, + boolean awsDualstack, boolean useVirtualStyle, + String region, Provider provider, OkHttpClient httpClient) { super( baseUrl, - region, - isAwsHost, - isFipsHost, - isAccelerateHost, - isDualStackHost, + awsS3Prefix, + awsDomainSuffix, + awsDualstack, useVirtualStyle, + region, provider, httpClient); } @@ -3213,87 +3213,66 @@ public static Builder builder() { /** Argument builder of {@link MinioClient}. */ public static final class Builder { private HttpUrl baseUrl; - private String region; - private boolean isAwsHost; - private boolean isFipsHost; - private boolean isAccelerateHost; - private boolean isDualStackHost; + private String awsS3Prefix; + private String awsDomainSuffix; + private boolean awsDualstack; private boolean useVirtualStyle; + + private String region; private Provider provider; private OkHttpClient httpClient; - private boolean isAwsChinaHost; - private String regionInUrl; + private void setAwsInfo(String host, boolean https) { + this.awsS3Prefix = null; + this.awsDomainSuffix = null; + this.awsDualstack = false; - private boolean isAwsFipsEndpoint(String endpoint) { - return endpoint.startsWith("s3-fips."); - } + if (!HttpUtils.HOSTNAME_REGEX.matcher(host).find()) return; - private boolean isAwsAccelerateEndpoint(String endpoint) { - return endpoint.startsWith("s3-accelerate."); - } + if (HttpUtils.AWS_ELB_ENDPOINT_REGEX.matcher(host).find()) { + String[] tokens = host.split("\\.elb\\.amazonaws\\.com", 1)[0].split("\\."); + this.region = tokens[tokens.length - 1]; + return; + } - private boolean isAwsEndpoint(String endpoint) { - return (endpoint.startsWith("s3.") - || isAwsFipsEndpoint(endpoint) - || isAwsAccelerateEndpoint(endpoint)) - && (endpoint.endsWith(".amazonaws.com") || endpoint.endsWith(".amazonaws.com.cn")); - } + if (!HttpUtils.AWS_ENDPOINT_REGEX.matcher(host).find()) return; - private boolean isAwsDualStackEndpoint(String endpoint) { - return endpoint.contains(".dualstack."); - } + if (!HttpUtils.AWS_S3_ENDPOINT_REGEX.matcher(host).find()) { + throw new IllegalArgumentException("invalid Amazon AWS host " + host); + } + + Matcher matcher = HttpUtils.AWS_S3_PREFIX_REGEX.matcher(host); + matcher.lookingAt(); + int end = matcher.end(); + + this.awsS3Prefix = host.substring(0, end); + if (this.awsS3Prefix.contains("s3-accesspoint") && !https) { + throw new IllegalArgumentException("use HTTPS scheme for host " + host); + } - /** - * Extracts region from AWS endpoint if available. Region is placed at second token normal - * endpoints and third token for dualstack endpoints. - * - *

Region is marked in square brackets in below examples. - *

-     * https://s3.[us-east-2].amazonaws.com
-     * https://s3.dualstack.[ca-central-1].amazonaws.com
-     * https://s3.[cn-north-1].amazonaws.com.cn
-     * https://s3.dualstack.[cn-northwest-1].amazonaws.com.cn
-     */
-    private String extractRegion(String endpoint) {
-      String[] tokens = endpoint.split("\\.");
-      String token = tokens[1];
-
-      // If token is "dualstack", then region might be in next token.
-      if (token.equals("dualstack")) {
-        token = tokens[2];
+      String[] tokens = host.substring(end).split("\\.");
+      awsDualstack = "dualstack".equals(tokens[0]);
+      if (awsDualstack) tokens = Arrays.copyOfRange(tokens, 1, tokens.length);
+      String regionInHost = null;
+      if (!tokens[0].equals("vpce") && !tokens[0].equals("amazonaws")) {
+        regionInHost = tokens[0];
+        tokens = Arrays.copyOfRange(tokens, 1, tokens.length);
       }
+      this.awsDomainSuffix = String.join(".", tokens);
 
-      // If token is equal to "amazonaws", region is not passed in the endpoint.
-      if (token.equals("amazonaws")) {
-        return null;
+      if (host.equals("s3-external-1.amazonaws.com")) regionInHost = "us-east-1";
+      if (host.equals("s3-us-gov-west-1.amazonaws.com")
+          || host.equals("s3-fips-us-gov-west-1.amazonaws.com")) {
+        regionInHost = "us-gov-west-1";
       }
 
-      // Return token as region.
-      return token;
+      if (regionInHost != null) this.region = regionInHost;
     }
 
     private void setBaseUrl(HttpUrl url) {
-      String host = url.host();
-
-      this.isAwsHost = isAwsEndpoint(host);
-      this.isAwsChinaHost = false;
-      if (this.isAwsHost) {
-        this.isAwsChinaHost = host.endsWith(".cn");
-        url =
-            url.newBuilder()
-                .host(this.isAwsChinaHost ? "amazonaws.com.cn" : "amazonaws.com")
-                .build();
-        this.isFipsHost = isAwsFipsEndpoint(host);
-        this.isAccelerateHost = isAwsAccelerateEndpoint(host);
-        this.isDualStackHost = isAwsDualStackEndpoint(host);
-        this.regionInUrl = extractRegion(host);
-        this.useVirtualStyle = true;
-      } else {
-        this.useVirtualStyle = host.endsWith("aliyuncs.com");
-      }
-
       this.baseUrl = url;
+      this.setAwsInfo(url.host(), url.isHttps());
+      this.useVirtualStyle = this.awsDomainSuffix != null || url.host().endsWith("aliyuncs.com");
     }
 
     public Builder endpoint(String endpoint) {
@@ -3325,8 +3304,9 @@ public Builder endpoint(HttpUrl url) {
     }
 
     public Builder region(String region) {
-      HttpUtils.validateNullOrNotEmptyString(region, "region");
-      this.regionInUrl = region;
+      if (region != null && !HttpUtils.REGION_REGEX.matcher(region).find()) {
+        throw new IllegalArgumentException("invalid region " + region);
+      }
       this.region = region;
       return this;
     }
@@ -3349,7 +3329,11 @@ public Builder httpClient(OkHttpClient httpClient) {
 
     public MinioAsyncClient build() {
       HttpUtils.validateNotNull(this.baseUrl, "endpoint");
-      if (this.isAwsChinaHost && this.regionInUrl == null && this.region == null) {
+
+      if (this.awsDomainSuffix != null
+          && this.awsDomainSuffix.endsWith(".cn")
+          && !this.awsS3Prefix.endsWith("s3-accelerate.")
+          && this.region == null) {
         throw new IllegalArgumentException(
             "Region missing in Amazon S3 China endpoint " + this.baseUrl);
       }
@@ -3358,17 +3342,15 @@ public MinioAsyncClient build() {
         this.httpClient =
             HttpUtils.newDefaultHttpClient(
                 DEFAULT_CONNECTION_TIMEOUT, DEFAULT_CONNECTION_TIMEOUT, DEFAULT_CONNECTION_TIMEOUT);
-        if (this.region == null) this.region = regionInUrl;
       }
 
       return new MinioAsyncClient(
           baseUrl,
-          region,
-          isAwsHost,
-          isFipsHost,
-          isAccelerateHost,
-          isDualStackHost,
+          awsS3Prefix,
+          awsDomainSuffix,
+          awsDualstack,
           useVirtualStyle,
+          region,
           provider,
           httpClient);
     }
diff --git a/api/src/main/java/io/minio/MinioClient.java b/api/src/main/java/io/minio/MinioClient.java
index 70094ad9e..0015d1728 100644
--- a/api/src/main/java/io/minio/MinioClient.java
+++ b/api/src/main/java/io/minio/MinioClient.java
@@ -2405,12 +2405,22 @@ public void traceOff() throws IOException {
     asyncClient.traceOff();
   }
 
-  /** Enables accelerate endpoint for Amazon S3 endpoint. */
+  /**
+   * Enables accelerate endpoint for Amazon S3 endpoint.
+   *
+   * @deprecated This method is no longer supported.
+   */
+  @Deprecated
   public void enableAccelerateEndpoint() {
     asyncClient.enableAccelerateEndpoint();
   }
 
-  /** Disables accelerate endpoint for Amazon S3 endpoint. */
+  /**
+   * Disables accelerate endpoint for Amazon S3 endpoint.
+   *
+   * @deprecated This method is no longer supported.
+   */
+  @Deprecated
   public void disableAccelerateEndpoint() {
     asyncClient.disableAccelerateEndpoint();
   }
@@ -2435,6 +2445,11 @@ public void disableVirtualStyleEndpoint() {
     asyncClient.disableVirtualStyleEndpoint();
   }
 
+  /** Sets AWS S3 domain prefix. */
+  public void setAwsS3Prefix(String awsS3Prefix) {
+    asyncClient.setAwsS3Prefix(awsS3Prefix);
+  }
+
   public static Builder builder() {
     return new Builder();
   }
diff --git a/api/src/main/java/io/minio/S3Base.java b/api/src/main/java/io/minio/S3Base.java
index aba3b0c9c..f9aca6086 100644
--- a/api/src/main/java/io/minio/S3Base.java
+++ b/api/src/main/java/io/minio/S3Base.java
@@ -87,6 +87,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.logging.Logger;
 import java.util.stream.Collectors;
+import javax.annotation.Nonnull;
 import okhttp3.Call;
 import okhttp3.Callback;
 import okhttp3.Headers;
@@ -103,7 +104,7 @@ public abstract class S3Base {
     try {
       RequestBody.create(new byte[] {}, null);
     } catch (NoSuchMethodError ex) {
-      throw new RuntimeException("Unsupported OkHttp library found. Must use okhttp >= 4.8.1", ex);
+      throw new RuntimeException("Unsupported OkHttp library found. Must use okhttp >= 4.11.0", ex);
     }
   }
 
@@ -128,16 +129,35 @@ public abstract class S3Base {
   private String userAgent = MinioProperties.INSTANCE.getDefaultUserAgent();
 
   protected HttpUrl baseUrl;
+  protected String awsS3Prefix;
+  protected String awsDomainSuffix;
+  protected boolean awsDualstack;
+  protected boolean useVirtualStyle;
   protected String region;
   protected Provider provider;
+  protected OkHttpClient httpClient;
 
-  private boolean isAwsHost;
-  private boolean isFipsHost;
-  private boolean isAccelerateHost;
-  private boolean isDualStackHost;
-  private boolean useVirtualStyle;
-  private OkHttpClient httpClient;
+  protected S3Base(
+      HttpUrl baseUrl,
+      String awsS3Prefix,
+      String awsDomainSuffix,
+      boolean awsDualstack,
+      boolean useVirtualStyle,
+      String region,
+      Provider provider,
+      OkHttpClient httpClient) {
+    this.baseUrl = baseUrl;
+    this.awsS3Prefix = awsS3Prefix;
+    this.awsDomainSuffix = awsDomainSuffix;
+    this.awsDualstack = awsDualstack;
+    this.useVirtualStyle = useVirtualStyle;
+    this.region = region;
+    this.provider = provider;
+    this.httpClient = httpClient;
+  }
 
+  /** @deprecated This method is no longer supported. */
+  @Deprecated
   protected S3Base(
       HttpUrl baseUrl,
       String region,
@@ -149,24 +169,28 @@ protected S3Base(
       Provider provider,
       OkHttpClient httpClient) {
     this.baseUrl = baseUrl;
-    this.region = region;
-    this.isAwsHost = isAwsHost;
-    this.isFipsHost = isFipsHost;
-    this.isAccelerateHost = isAccelerateHost;
-    this.isDualStackHost = isDualStackHost;
+    if (isAwsHost) this.awsS3Prefix = "s3.";
+    if (isFipsHost) this.awsS3Prefix = "s3-fips.";
+    if (isAccelerateHost) this.awsS3Prefix = "s3-accelerate.";
+    if (isAwsHost || isFipsHost || isAccelerateHost) {
+      String host = baseUrl.host();
+      if (host.endsWith(".amazonaws.com")) this.awsDomainSuffix = "amazonaws.com";
+      if (host.endsWith(".amazonaws.com.cn")) this.awsDomainSuffix = "amazonaws.com.cn";
+    }
+    this.awsDualstack = isDualStackHost;
     this.useVirtualStyle = useVirtualStyle;
+    this.region = region;
     this.provider = provider;
     this.httpClient = httpClient;
   }
 
   protected S3Base(S3Base client) {
     this.baseUrl = client.baseUrl;
-    this.region = client.region;
-    this.isAwsHost = client.isAwsHost;
-    this.isFipsHost = client.isFipsHost;
-    this.isAccelerateHost = client.isAccelerateHost;
-    this.isDualStackHost = client.isDualStackHost;
+    this.awsS3Prefix = client.awsS3Prefix;
+    this.awsDomainSuffix = client.awsDomainSuffix;
+    this.awsDualstack = client.awsDualstack;
     this.useVirtualStyle = client.useVirtualStyle;
+    this.region = client.region;
     this.provider = client.provider;
     this.httpClient = client.httpClient;
   }
@@ -174,6 +198,18 @@ protected S3Base(S3Base client) {
   /** Check whether argument is valid or not. */
   protected void checkArgs(BaseArgs args) {
     if (args == null) throw new IllegalArgumentException("null arguments");
+
+    if ((this.awsDomainSuffix != null) && (args instanceof BucketArgs)) {
+      String bucketName = ((BucketArgs) args).bucket();
+      if (bucketName.startsWith("xn--")
+          || bucketName.endsWith("--s3alias")
+          || bucketName.endsWith("--ol-s3")) {
+        throw new IllegalArgumentException(
+            "bucket name '"
+                + bucketName
+                + "' must not start with 'xn--' and must not end with '--s3alias' or '--ol-s3'");
+      }
+    }
   }
 
   /** Merge two Multimaps. */
@@ -275,6 +311,56 @@ private String[] handleRedirectResponse(
     return new String[] {code, message};
   }
 
+  private String buildAwsUrl(
+      HttpUrl.Builder builder, String bucketName, boolean enforcePathStyle, String region) {
+    String host = this.awsS3Prefix + this.awsDomainSuffix;
+    if (host.equals("s3-external-1.amazonaws.com")
+        || host.equals("s3-us-gov-west-1.amazonaws.com")
+        || host.equals("s3-fips-us-gov-west-1.amazonaws.com")) {
+      builder.host(host);
+      return host;
+    }
+
+    host = this.awsS3Prefix;
+    if (this.awsS3Prefix.contains("s3-accelerate")) {
+      if (bucketName.contains(".")) {
+        throw new IllegalArgumentException(
+            "bucket name '" + bucketName + "' with '.' is not allowed for accelerate endpoint");
+      }
+      if (enforcePathStyle) host = host.replaceFirst("-accelerate", "");
+    }
+
+    if (this.awsDualstack) host += "dualstack.";
+    if (!this.awsS3Prefix.contains("s3-accelerate")) host += region + ".";
+    host += this.awsDomainSuffix;
+
+    builder.host(host);
+    return host;
+  }
+
+  private String buildListBucketsUrl(HttpUrl.Builder builder, String region) {
+    if (this.awsDomainSuffix == null) return null;
+
+    String host = this.awsS3Prefix + this.awsDomainSuffix;
+    if (host.equals("s3-external-1.amazonaws.com")
+        || host.equals("s3-us-gov-west-1.amazonaws.com")
+        || host.equals("s3-fips-us-gov-west-1.amazonaws.com")) {
+      builder.host(host);
+      return host;
+    }
+
+    String s3Prefix = this.awsS3Prefix;
+    String domainSuffix = this.awsDomainSuffix;
+    if (this.awsS3Prefix.startsWith("s3.") || this.awsS3Prefix.startsWith("s3-")) {
+      s3Prefix = "s3.";
+      domainSuffix = "amazonaws.com" + (domainSuffix.endsWith(".cn") ? ".cn" : "");
+    }
+
+    host = s3Prefix + region + "." + domainSuffix;
+    builder.host(host);
+    return host;
+  }
+
   /** Build URL for given parameters. */
   protected HttpUrl buildUrl(
       Method method,
@@ -288,72 +374,43 @@ protected HttpUrl buildUrl(
     }
 
     HttpUrl.Builder urlBuilder = this.baseUrl.newBuilder();
-    String host = this.baseUrl.host();
-    if (bucketName != null) {
-      boolean enforcePathStyle = false;
-      if (method == Method.PUT && objectName == null && queryParamMap == null) {
-        // use path style for make bucket to workaround "AuthorizationHeaderMalformed" error from
-        // s3.amazonaws.com
-        enforcePathStyle = true;
-      } else if (queryParamMap != null && queryParamMap.containsKey("location")) {
-        // use path style for location query
-        enforcePathStyle = true;
-      } else if (bucketName.contains(".") && this.baseUrl.isHttps()) {
-        // use path style where '.' in bucketName causes SSL certificate validation error
-        enforcePathStyle = true;
-      }
 
-      if (isAwsHost) {
-        String s3Domain = "s3.";
-        if (isFipsHost) {
-          s3Domain = "s3-fips.";
-        } else if (isAccelerateHost) {
-          if (bucketName.contains(".")) {
-            throw new IllegalArgumentException(
-                "bucket name '"
-                    + bucketName
-                    + "' with '.' is not allowed for accelerated endpoint");
-          }
-
-          if (!enforcePathStyle) s3Domain = "s3-accelerate.";
-        }
+    if (queryParamMap != null) {
+      for (Map.Entry entry : queryParamMap.entries()) {
+        urlBuilder.addEncodedQueryParameter(
+            S3Escaper.encode(entry.getKey()), S3Escaper.encode(entry.getValue()));
+      }
+    }
 
-        String dualStack = "";
-        if (isDualStackHost) dualStack = "dualstack.";
+    if (bucketName == null) {
+      this.buildListBucketsUrl(urlBuilder, region);
+      return urlBuilder.build();
+    }
 
-        String endpoint = s3Domain + dualStack;
-        if (enforcePathStyle || !isAccelerateHost) endpoint += region + ".";
+    boolean enforcePathStyle = (
+        // use path style for make bucket to workaround "AuthorizationHeaderMalformed" error from
+        // s3.amazonaws.com
+        (method == Method.PUT && objectName == null && queryParamMap == null)
 
-        host = endpoint + host;
-      }
+            // use path style for location query
+            || (queryParamMap != null && queryParamMap.containsKey("location"))
 
-      if (enforcePathStyle || !useVirtualStyle) {
-        urlBuilder.host(host);
-        urlBuilder.addEncodedPathSegment(S3Escaper.encode(bucketName));
-      } else {
-        urlBuilder.host(bucketName + "." + host);
-      }
+            // use path style where '.' in bucketName causes SSL certificate validation error
+            || (bucketName.contains(".") && this.baseUrl.isHttps()));
 
-      if (objectName != null) {
-        // Limitation: OkHttp does not allow to add '.' and '..' as path segment.
-        for (String token : objectName.split("/")) {
-          if (token.equals(".") || token.equals("..")) {
-            throw new IllegalArgumentException(
-                "object name with '.' or '..' path segment is not supported");
-          }
-        }
+    String host = this.baseUrl.host();
+    if (this.awsDomainSuffix != null) {
+      host = this.buildAwsUrl(urlBuilder, bucketName, enforcePathStyle, region);
+    }
 
-        urlBuilder.addEncodedPathSegments(S3Escaper.encodePath(objectName));
-      }
+    if (enforcePathStyle || !this.useVirtualStyle) {
+      urlBuilder.addEncodedPathSegment(S3Escaper.encode(bucketName));
     } else {
-      if (isAwsHost) urlBuilder.host("s3." + region + "." + host);
+      urlBuilder.host(bucketName + "." + host);
     }
 
-    if (queryParamMap != null) {
-      for (Map.Entry entry : queryParamMap.entries()) {
-        urlBuilder.addEncodedQueryParameter(
-            S3Escaper.encode(entry.getKey()), S3Escaper.encode(entry.getValue()));
-      }
+    if (objectName != null) {
+      urlBuilder.addEncodedPathSegments(S3Escaper.encodePath(objectName));
     }
 
     return urlBuilder.build();
@@ -847,7 +904,7 @@ protected CompletableFuture getRegionAsync(String bucketName, String reg
             LocationConstraint lc = Xml.unmarshal(LocationConstraint.class, body.charStream());
             if (lc.location() == null || lc.location().equals("")) {
               location = US_EAST_1;
-            } else if (lc.location().equals("EU")) {
+            } else if (lc.location().equals("EU") && this.awsDomainSuffix != null) {
               location = "eu-west-1"; // eu-west-1 is also referred as 'EU'.
             } else {
               location = lc.location();
@@ -1678,24 +1735,34 @@ public void traceOff() throws IOException {
     this.traceStream = null;
   }
 
-  /** Enables accelerate endpoint for Amazon S3 endpoint. */
+  /**
+   * Enables accelerate endpoint for Amazon S3 endpoint.
+   *
+   * @deprecated This method is no longer supported.
+   */
+  @Deprecated
   public void enableAccelerateEndpoint() {
-    this.isAccelerateHost = true;
+    this.awsS3Prefix = "s3-accelerate.";
   }
 
-  /** Disables accelerate endpoint for Amazon S3 endpoint. */
+  /**
+   * Disables accelerate endpoint for Amazon S3 endpoint.
+   *
+   * @deprecated This method is no longer supported.
+   */
+  @Deprecated
   public void disableAccelerateEndpoint() {
-    this.isAccelerateHost = false;
+    this.awsS3Prefix = "s3.";
   }
 
   /** Enables dual-stack endpoint for Amazon S3 endpoint. */
   public void enableDualStackEndpoint() {
-    this.isDualStackHost = true;
+    this.awsDualstack = true;
   }
 
   /** Disables dual-stack endpoint for Amazon S3 endpoint. */
   public void disableDualStackEndpoint() {
-    this.isDualStackHost = false;
+    this.awsDualstack = false;
   }
 
   /** Enables virtual-style endpoint. */
@@ -1708,6 +1775,15 @@ public void disableVirtualStyleEndpoint() {
     this.useVirtualStyle = false;
   }
 
+  /** Sets AWS S3 domain prefix. */
+  public void setAwsS3Prefix(@Nonnull String awsS3Prefix) {
+    if (awsS3Prefix == null) throw new IllegalArgumentException("null Amazon AWS S3 domain prefix");
+    if (!HttpUtils.AWS_S3_PREFIX_REGEX.matcher(awsS3Prefix).find()) {
+      throw new IllegalArgumentException("invalid Amazon AWS S3 domain prefix " + awsS3Prefix);
+    }
+    this.awsS3Prefix = awsS3Prefix;
+  }
+
   /** Execute stat object asynchronously. */
   protected CompletableFuture statObjectAsync(StatObjectArgs args)
       throws InsufficientDataException, InternalException, InvalidKeyException, IOException,
diff --git a/api/src/main/java/io/minio/http/HttpUtils.java b/api/src/main/java/io/minio/http/HttpUtils.java
index ea59e1b77..b69f3d2c2 100644
--- a/api/src/main/java/io/minio/http/HttpUtils.java
+++ b/api/src/main/java/io/minio/http/HttpUtils.java
@@ -31,6 +31,7 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.KeyManager;
 import javax.net.ssl.KeyManagerFactory;
@@ -46,6 +47,36 @@
 
 /** HTTP utilities. */
 public class HttpUtils {
+  public static final String AWS_S3_PREFIX =
+      "^(((bucket\\.|accesspoint\\.)"
+          + "vpce(-(?!_)[a-z_\\d]+(? 253) {
-      throw new IllegalArgumentException("invalid hostname");
-    }
-
-    for (String label : endpoint.split("\\.")) {
-      if (label.length() < 1 || label.length() > 63) {
-        throw new IllegalArgumentException("invalid hostname");
-      }
-
-      if (!(label.matches("^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$"))) {
-        throw new IllegalArgumentException("invalid hostname");
-      }
+    if (!HOSTNAME_REGEX.matcher(endpoint).find()) {
+      throw new IllegalArgumentException("invalid hostname " + endpoint);
     }
   }