io.nitor.api.backend.s3.S3Handler.java Source code

Java tutorial

Introduction

Here is the source code for io.nitor.api.backend.s3.S3Handler.java

Source

/*
 * Copyright 2017-2018 Nitor Creations Oy
 *
 * 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 io.nitor.api.backend.s3;

import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpClientResponse;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.streams.Pump;
import io.vertx.ext.web.RoutingContext;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.nitor.api.backend.util.Helpers.resolveRegion;
import static io.nitor.api.backend.util.Helpers.resolveCredentialsProvider;
import static io.vertx.core.http.HttpVersion.HTTP_1_1;
import static java.util.concurrent.TimeUnit.SECONDS;

public class S3Handler implements Handler<RoutingContext> {
    private static final Logger logger = LogManager.getLogger(S3Handler.class);

    private final String s3Host;
    private final List<PathComponent> basePathComponents;
    private final Set<HttpMethod> allowedMethods = new HashSet<>();
    private final HttpClient http;
    private final AWSRequestSigner signer;
    private final int routeLength;
    private final String indexFile;
    private final Pattern staticPaths;

    public S3Handler(Vertx vertx, JsonObject conf, int routeLength) {
        this.routeLength = routeLength;

        indexFile = conf.getString("indexFile", "index.html");

        String staticPathConfig = conf.getString("staticPaths");
        staticPaths = staticPathConfig != null ? Pattern.compile(staticPathConfig) : null;

        String region = resolveRegion(conf).toString();
        this.s3Host = ("us-east-1".equals(region) ? "s3" : "s3-" + region) + ".amazonaws.com";

        String bucket = conf.getString("bucket");
        String basePath = '/' + bucket + '/' + conf.getString("path", "");
        basePathComponents = PathComponent.splitPath(basePath);

        AwsCredentialsProvider secretsProvider = resolveCredentialsProvider(conf);
        signer = new AWSRequestSigner(region, s3Host, secretsProvider);

        JsonArray operations = conf.getJsonArray("operations", new JsonArray().add("GET"));
        operations.forEach(op -> allowedMethods.add(HttpMethod.valueOf(op.toString())));

        http = vertx.createHttpClient(new HttpClientOptions()
                .setConnectTimeout((int) SECONDS.toMillis(conf.getInteger("connectTimeout", 5)))
                .setIdleTimeout((int) SECONDS.toSeconds(conf.getInteger("idleTimeout", 60)))
                .setMaxPoolSize(conf.getInteger("maxPoolSize", 100)).setPipelining(false).setMaxWaitQueueSize(100)
                .setUsePooledBuffers(true).setProtocolVersion(HTTP_1_1).setMaxRedirects(5)
                .setTryUseCompression(false));
    }

    @Override
    public void handle(RoutingContext ctx) {
        HttpServerRequest sreq = ctx.request();
        String path = sreq.path();
        if (path.endsWith("/")) {
            path += indexFile;
        }
        if (path.contains("../")) {
            ctx.response().setStatusCode(404).end();
            return;
        }
        path = path.substring(routeLength);
        if (path.startsWith("/")) {
            path = path.substring(1);
        }
        if (staticPaths != null && !staticPaths.matcher(path).matches()) {
            path = indexFile;
        }
        StringBuilder sb = new StringBuilder();
        for (PathComponent component : basePathComponents) {
            if (!component.appendTo(ctx, sb)) {
                ctx.response().setStatusCode(BAD_REQUEST.code()).end();
                return;
            }
        }
        sb.append(path);
        path = sb.toString();

        if (!allowedMethods.contains(sreq.method())) {
            ctx.response().setStatusCode(BAD_REQUEST.code()).end();
            return;
        }

        switch (sreq.method()) {
        case GET:
            HttpClientRequest creq = http.get(s3Host, path);
            prepareRequest(sreq, creq);
            HttpServerResponse sres = ctx.response();
            sres.closeHandler(close -> creq.connection().close());
            creq.handler(cres -> mapResponse(cres, sres));
            creq.end();
            break;
        default:
            ctx.response().setStatusCode(BAD_REQUEST.code()).end();
        }
    }

    private void prepareRequest(HttpServerRequest sreq, HttpClientRequest creq) {
        signer.copyHeadersAndSign(sreq, creq, null);
    }

    private void mapResponse(HttpClientResponse cres, HttpServerResponse sres) {
        cres.exceptionHandler(t -> {
            logger.error("Error processing s3 request", t);
            if (!sres.ended()) {
                sres.setStatusCode(502);
                sres.end();
            }
        });

        sres.setStatusCode(cres.statusCode());
        sres.setStatusMessage(cres.statusMessage());
        if (cres.statusCode() != 200 && cres.statusCode() != 206) {
            sres.end();
            return;
        }

        MultiMap headers = sres.headers();
        cres.headers().forEach(entry -> {
            String key = entry.getKey();
            if (key.startsWith("x-amz-")) {
                return;
            }
            String lKey = key.toLowerCase();
            if ("server".equals(lKey) || "accept-ranges".equals(lKey) || "transfer-encoding".equals(lKey)
                    || "date".equals(lKey) || "connection".equals(lKey)) {
                return;
            }
            headers.add(key, entry.getValue());
        });
        // TODO handle http 1.0 that requires connection header

        Pump resPump = Pump.pump(cres, sres);
        cres.endHandler(v -> sres.end());
        resPump.start();
    }

}