com.vmware.dcp.common.http.netty.NettyHttpClientRequestHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.vmware.dcp.common.http.netty.NettyHttpClientRequestHandler.java

Source

/*
 * Copyright (c) 2014-2015 VMware, Inc. 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.vmware.dcp.common.http.netty;

import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;

import javax.net.ssl.SSLSession;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.AsciiString;
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.HttpHeaderUtil;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.ssl.SslHandler;

import com.vmware.dcp.common.Operation;
import com.vmware.dcp.common.Operation.AuthorizationContext;
import com.vmware.dcp.common.Service.Action;
import com.vmware.dcp.common.ServiceErrorResponse;
import com.vmware.dcp.common.ServiceHost;
import com.vmware.dcp.common.UriUtils;
import com.vmware.dcp.common.Utils;
import com.vmware.dcp.services.common.authn.AuthenticationConstants;

/**
 * Processes client requests on behalf of the HTTP listener and submits them to the service host or websocket client for
 * processing
 */
public class NettyHttpClientRequestHandler extends SimpleChannelInboundHandler<Object> {

    private final ServiceHost host;

    public NettyHttpClientRequestHandler(ServiceHost host) {
        this.host = host;
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    protected void messageReceived(ChannelHandlerContext ctx, Object msg) {
        Operation request = null;
        try {
            request = ctx.channel().attr(NettyChannelContext.OPERATION_KEY).get();
            if (!(msg instanceof FullHttpRequest)) {
                return;
            }
            // Start of request processing, initialize in-bound operation
            FullHttpRequest nettyRequest = (FullHttpRequest) msg;

            long expMicros = Utils.getNowMicrosUtc() + this.host.getOperationTimeoutMicros();
            URI targetUri = new URI(nettyRequest.uri());
            request = Operation.createGet(null);
            request.setAction(Action.valueOf(nettyRequest.method().toString())).setExpiration(expMicros);
            request.setUri(UriUtils.buildUri(this.host, targetUri.getPath(), targetUri.getQuery()));
            ctx.channel().attr(NettyChannelContext.OPERATION_KEY).set(request);

            if (nettyRequest.decoderResult().isFailure()) {
                request.setStatusCode(Operation.STATUS_CODE_BAD_REQUEST);
                request.setBody(
                        ServiceErrorResponse.create(nettyRequest.decoderResult().cause(), request.getStatusCode()));
                sendResponse(ctx, request);
                return;
            }
            parseRequestHeaders(ctx, request, nettyRequest);
            decodeRequestBody(ctx, request, nettyRequest.content());
        } catch (Throwable e) {
            this.host.log(Level.SEVERE, "Uncaught exception: %s", Utils.toString(e));
            if (request == null) {
                request = Operation.createGet(this.host.getUri());
            }
            int sc = Operation.STATUS_CODE_BAD_REQUEST;
            if (e instanceof URISyntaxException) {
                request.setUri(this.host.getUri());
            }
            request.setStatusCode(sc).setBodyNoCloning(ServiceErrorResponse.create(e, sc));
            sendResponse(ctx, request);
        }
    }

    private void decodeRequestBody(ChannelHandlerContext ctx, Operation request, ByteBuf content) {
        if (!content.isReadable()) {
            // skip body decode, request had no body
            request.setContentLength(0);
            submitRequest(ctx, request);
            return;
        }

        request.nestCompletion((o, e) -> {
            if (e != null) {
                request.setStatusCode(Operation.STATUS_CODE_BAD_REQUEST);
                request.setBody(ServiceErrorResponse.create(e, request.getStatusCode()));
                sendResponse(ctx, request);
                return;
            }
            submitRequest(ctx, request);
        });

        Utils.decodeBody(request, content.nioBuffer());
    }

    private void parseRequestHeaders(ChannelHandlerContext ctx, Operation request, HttpRequest nettyRequest) {
        HttpHeaders headers = nettyRequest.headers();

        String referer = headers.getAndRemoveAndConvert(HttpHeaderNames.REFERER);
        if (referer != null) {
            try {
                request.setReferer(new URI(referer));
            } catch (URISyntaxException e) {
                setRefererFromSocketContext(ctx, request);
            }
        } else {
            setRefererFromSocketContext(ctx, request);
        }

        if (headers.isEmpty()) {
            return;
        }

        request.setKeepAlive(HttpHeaderUtil.isKeepAlive(nettyRequest));
        if (HttpHeaderUtil.isContentLengthSet(nettyRequest)) {
            request.setContentLength(HttpHeaderUtil.getContentLength(nettyRequest));
        }

        request.setContextId(headers.getAndRemoveAndConvert(Operation.CONTEXT_ID_HEADER));

        String contentType = headers.getAndRemoveAndConvert(HttpHeaderNames.CONTENT_TYPE);
        if (contentType != null) {
            request.setContentType(contentType);
        }

        String cookie = headers.getAndRemoveAndConvert(HttpHeaderNames.COOKIE);
        if (cookie != null) {
            request.setCookies(CookieJar.decodeCookies(cookie));
        }

        for (Entry<String, String> h : headers.entriesConverted()) {
            String key = h.getKey();
            String value = h.getValue();
            request.addRequestHeader(key, value);
        }

        // Add peer Principal and CertificateChain to operation in case if client was successfully authenticated
        // by Netty using client certificate
        SslHandler sslHandler = (SslHandler) ctx.channel().pipeline().get(NettyHttpServerInitializer.SSL_HANDLER);
        if (sslHandler != null) {
            try {
                if (sslHandler.engine().getWantClientAuth() || sslHandler.engine().getNeedClientAuth()) {
                    SSLSession session = sslHandler.engine().getSession();
                    request.setPeerCertificates(session.getPeerPrincipal(), session.getPeerCertificateChain());
                }
            } catch (Exception e) {
                this.host.log(Level.FINE, "Failed to get peer principal " + Utils.toString(e));
            }
        }
    }

    private void submitRequest(ChannelHandlerContext ctx, Operation request) {
        request.nestCompletion((o, e) -> {
            request.setBodyNoCloning(o.getBodyRaw());
            sendResponse(ctx, request);
        });

        Operation localOp = request;
        if (request.getRequestCallbackLocation() != null) {
            localOp = processRequestWithCallback(request);
        }

        this.host.handleRequest(null, localOp);
    }

    /**
     * Handles an operation that is split into the asynchronous HTTP pattern.
     * <p/>
     * 1) complete operation from remote peer immediately with ACCEPTED
     * <p/>
     * 2) Create a new local operation, cloned from the remote peer op, and set a completion that
     * will generate a PATCH to the remote callback location
     *
     * @param op
     * @return
     */
    private Operation processRequestWithCallback(Operation op) {
        final URI[] targetCallback = { null };
        try {
            targetCallback[0] = new URI(op.getRequestCallbackLocation());
        } catch (URISyntaxException e1) {
            op.fail(e1);
            return null;
        }

        Operation localOp = op.clone();

        // complete remote operation eagerly. We will PATCH the callback location with the
        // result when the local operation completes
        op.setStatusCode(Operation.STATUS_CODE_ACCEPTED).setBody(null).complete();

        localOp.setCompletion((o, e) -> {
            Operation patchForCompletion = Operation.createPatch(targetCallback[0]).setReferer(o.getUri());
            int responseStatusCode = o.getStatusCode();
            if (e != null) {
                ServiceErrorResponse rsp = Utils.toServiceErrorResponse(e);
                rsp.statusCode = responseStatusCode;
                patchForCompletion.setBody(rsp);
            } else {
                if (!o.hasBody()) {
                    patchForCompletion.setBodyNoCloning(Operation.EMPTY_JSON_BODY);
                } else {
                    patchForCompletion.setBodyNoCloning(o.getBodyRaw());
                }
            }

            patchForCompletion.transferResponseHeadersToRequestHeadersFrom(o);
            patchForCompletion.addRequestHeader(Operation.RESPONSE_CALLBACK_STATUS_HEADER,
                    Integer.toString(responseStatusCode));
            this.host.sendRequest(patchForCompletion);
        });

        return localOp;
    }

    private void sendResponse(ChannelHandlerContext ctx, Operation request) {
        try {
            writeResponseUnsafe(ctx, request);
        } catch (Throwable e1) {
            this.host.log(Level.SEVERE, "%s", Utils.toString(e1));
        }
    }

    private void writeResponseUnsafe(ChannelHandlerContext ctx, Operation request) {
        ByteBuf bodyBuffer = null;
        FullHttpResponse response;
        try {
            byte[] data = Utils.encodeBody(request);
            if (data != null) {
                bodyBuffer = Unpooled.wrappedBuffer(data);
            }
        } catch (Throwable e1) {
            // Note that this is a program logic error - some service isn't properly checking or setting Content-Type
            this.host.log(Level.SEVERE, "Error encoding body: %s", Utils.toString(e1));
            byte[] data;
            try {
                data = ("Error encoding body: " + e1.getMessage()).getBytes(Utils.CHARSET);
            } catch (UnsupportedEncodingException ueex) {
                this.exceptionCaught(ctx, ueex);
                return;
            }
            response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR,
                    Unpooled.wrappedBuffer(data));
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, request.getContentType());
            response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
            writeResponse(ctx, request, response);
            return;
        }

        if (bodyBuffer == null || request.getStatusCode() == Operation.STATUS_CODE_NOT_MODIFIED) {
            response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
                    HttpResponseStatus.valueOf(request.getStatusCode()));
        } else {
            response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
                    HttpResponseStatus.valueOf(request.getStatusCode()), bodyBuffer);
        }

        response.headers().set(HttpHeaderNames.CONTENT_TYPE, request.getContentType());
        response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());

        // add any other custom headers associated with operation
        for (Entry<String, String> nameValue : request.getResponseHeaders().entrySet()) {
            response.headers().set(nameValue.getKey(), nameValue.getValue());
        }

        // Add Set-Cookie header to response if authorization context is marked as internal.
        AuthorizationContext authorizationContext = request.getAuthorizationContext();
        if (authorizationContext != null && authorizationContext.shouldPropagateToClient()) {
            StringBuilder buf = new StringBuilder().append(AuthenticationConstants.DCP_JWT_COOKIE).append('=')
                    .append(authorizationContext.getToken());

            // Add Path qualifier, cookie applies everywhere
            buf.append("; Path=/");
            // Add an Max-Age qualifier if an expiry is set in the Claims object
            if (authorizationContext.getClaims().getExpirationTime() != null) {
                buf.append("; Max-Age=");
                long maxAge = authorizationContext.getClaims().getExpirationTime() - Utils.getNowMicrosUtc();
                buf.append(maxAge > 0 ? TimeUnit.MICROSECONDS.toSeconds(maxAge) : 0);
            }
            response.headers().add(Operation.SET_COOKIE_HEADER, buf.toString());
        }

        writeResponse(ctx, request, response);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        Operation op = ctx.attr(NettyChannelContext.OPERATION_KEY).get();
        if (op != null) {
            this.host.log(Level.SEVERE, "Listener channel exception: %s, in progress op: %s", cause.getMessage(),
                    op.toString());
        }
        ctx.channel().attr(NettyChannelContext.OPERATION_KEY).remove();
        ctx.close();
    }

    private void setRefererFromSocketContext(ChannelHandlerContext ctx, Operation request) {
        try {
            InetSocketAddress remote = (InetSocketAddress) ctx.channel().remoteAddress();
            String path = NettyHttpListener.UNKNOWN_CLIENT_REFERER_PATH;
            request.setReferer(UriUtils.buildUri(remote.getHostString(), remote.getPort(), path, null));
        } catch (Throwable e) {
            this.host.log(Level.SEVERE, "%s", Utils.toString(e));
        }
    }

    private void writeResponse(ChannelHandlerContext ctx, Operation request, FullHttpResponse response) {
        boolean isClose = !request.isKeepAlive() || response == null;
        Object rsp = Unpooled.EMPTY_BUFFER;
        if (response != null) {
            AsciiString v = isClose ? HttpHeaderValues.CLOSE : HttpHeaderValues.KEEP_ALIVE;
            response.headers().set(HttpHeaderNames.CONNECTION, v);
            rsp = response;
        }

        ctx.channel().attr(NettyChannelContext.OPERATION_KEY).remove();

        ChannelFuture future = ctx.writeAndFlush(rsp);
        if (isClose) {
            future.addListener(ChannelFutureListener.CLOSE);
        }
    }
}