blob: a0b028bf5a91a73e21a2063dc01d1ef94cbe7e3b [file] [log] [blame]
// Copyright 2018 The Bazel Authors. All rights reserved.
//
// 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 com.google.devtools.build.remote.worker.http;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.CharsetUtil;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** A simple HTTP REST in-memory cache used during testing the LRE. */
@Sharable
public class HttpCacheServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private static final Pattern URI_PATTERN = Pattern.compile("^/?(.*/)?(ac/|cas/)([a-f0-9]{64})$");
private final ConcurrentMap<String, byte[]> cache;
@VisibleForTesting
public HttpCacheServerHandler(ConcurrentMap<String, byte[]> cache) {
this.cache = Preconditions.checkNotNull(cache);
}
HttpCacheServerHandler() {
this(new ConcurrentHashMap<>());
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
if (!request.decoderResult().isSuccess()) {
sendError(ctx, request, HttpResponseStatus.BAD_REQUEST);
return;
}
if (request.method().equals(HttpMethod.GET)) {
handleGet(ctx, request);
} else if (request.method().equals(HttpMethod.PUT)) {
handlePut(ctx, request);
} else {
sendError(ctx, request, HttpResponseStatus.METHOD_NOT_ALLOWED);
}
}
@VisibleForTesting
static boolean isUriValid(String uri) {
Matcher matcher = URI_PATTERN.matcher(uri);
return matcher.matches();
}
private void handleGet(ChannelHandlerContext ctx, FullHttpRequest request) {
if (!isUriValid(request.uri())) {
sendError(ctx, request, HttpResponseStatus.BAD_REQUEST);
return;
}
byte[] contents = cache.get(request.uri());
if (contents == null) {
sendError(ctx, request, HttpResponseStatus.NOT_FOUND);
return;
}
FullHttpResponse response =
new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(contents));
HttpUtil.setContentLength(response, contents.length);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/octet-stream");
ChannelFuture lastContentFuture = ctx.writeAndFlush(response);
if (!HttpUtil.isKeepAlive(request)) {
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
}
private void handlePut(ChannelHandlerContext ctx, FullHttpRequest request) {
if (!request.decoderResult().isSuccess()) {
sendError(ctx, request, HttpResponseStatus.INTERNAL_SERVER_ERROR);
return;
}
if (!isUriValid(request.uri())) {
sendError(ctx, request, HttpResponseStatus.BAD_REQUEST);
return;
}
byte[] contentBytes = new byte[request.content().readableBytes()];
request.content().readBytes(contentBytes);
cache.putIfAbsent(request.uri(), contentBytes);
FullHttpResponse response =
new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT);
ChannelFuture lastContentFuture = ctx.writeAndFlush(response);
if (!HttpUtil.isKeepAlive(request)) {
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
}
private static void sendError(
ChannelHandlerContext ctx, FullHttpRequest request, HttpResponseStatus status) {
ByteBuf data = Unpooled.copiedBuffer("Failure: " + status + "\r\n", CharsetUtil.UTF_8);
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, data);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, data.readableBytes());
ChannelFuture future = ctx.writeAndFlush(response);
if (!HttpUtil.isKeepAlive(request)) {
future.addListener(ChannelFutureListener.CLOSE);
}
}
}