All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.yamcs.http.ApiHandler Maven / Gradle / Ivy

There is a newer version: 5.10.9
Show newest version
package org.yamcs.http;

import static org.yamcs.http.HttpRequestHandler.CTX_CONTEXT;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;

import org.yamcs.YConfiguration;
import org.yamcs.security.User;

import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Message;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.WriteBufferWaterMark;
import io.netty.handler.codec.http.HttpContentCompressor;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.timeout.IdleStateHandler;

public class ApiHandler extends HttpHandler {

    private HttpServer httpServer;
    private RouteHandler routeHandler;
    private YConfiguration wsConfig;

    public ApiHandler(HttpServer httpServer) {
        this.httpServer = httpServer;

        var maxPageSize = httpServer.getConfig().getInt("maxPageSize");
        routeHandler = new RouteHandler(maxPageSize);

        wsConfig = httpServer.getConfig().getConfig("webSocket");
    }

    @Override
    public boolean requireAuth() {
        return true;
    }

    @Override
    public void handle(HandlerContext ctx) {
        /*
         * At this point we do not have the full request (only the header) so we have to configure the pipeline either
         * for receiving the full request or with route specific pipeline for receiving (large amounts of) data in case
         * of dataLoad routes.
         */

        var nettyContext = ctx.getNettyChannelHandlerContext();
        var nettyRequest = ctx.getNettyHttpRequest();
        var uri = HttpUtils.getPathWithoutContext(nettyRequest, ctx.getContextPath());
        if (uri.equals(HttpServer.WEBSOCKET_ROUTE.getGet())) {
            if (nettyRequest.method() == HttpMethod.GET) {
                prepareChannelForWebSocketUpgrade(nettyContext, nettyRequest, ctx.getUser());
                return;
            } else {
                throw new MethodNotAllowedException(nettyRequest.method(), uri, Arrays.asList(HttpMethod.GET));
            }
        }

        var match = matchRoute(nettyRequest.method(), uri);
        if (match == null) {
            throw new NotFoundException();
        }

        var routeContext = new RouteContext(httpServer, nettyContext, ctx.getUser(), nettyRequest, match.route,
                match.regexMatch);
        log.debug("{}: Routing {} {}", routeContext, nettyRequest.method(), nettyRequest.uri());

        nettyContext.channel().attr(CTX_CONTEXT).set(routeContext);

        var pipeline = nettyContext.pipeline();

        if (routeContext.isClientStreaming()) {
            pipeline.addLast(new HttpContentToByteBufDecoder());
            pipeline.addLast(new ProtobufVarint32FrameDecoder());

            String body = routeContext.getBodySpecifier();
            Message bodyPrototype = routeContext.getRequestPrototype();
            if (body != null && !"*".equals(body)) {
                FieldDescriptor field = bodyPrototype.getDescriptorForType().findFieldByName(body);
                bodyPrototype = bodyPrototype.newBuilderForType().getFieldBuilder(field)
                        .getDefaultInstanceForType();
            }
            pipeline.addLast(new ProtobufDecoder(bodyPrototype));
            pipeline.addLast(new StreamingClientHandler(routeContext));

            if (HttpUtil.is100ContinueExpected(nettyRequest)) {
                nettyContext.writeAndFlush(HttpUtils.CONTINUE_RESPONSE.retainedDuplicate());
            }
        } else {
            pipeline.addLast(new HttpContentCompressor());

            // this will cause the routeHandler read to be called as soon as the request is complete
            // it will also reject requests whose body is greater than the MAX_BODY_SIZE)
            pipeline.addLast(new HttpObjectAggregator(routeContext.getMaxBodySize()));
            pipeline.addLast(routeHandler);
            nettyContext.fireChannelRead(nettyRequest);
        }
    }

    /**
     * Adapts Netty's pipeline for allowing WebSocket upgrade
     *
     * @param ctx
     *            context for this channel handler
     */
    private void prepareChannelForWebSocketUpgrade(ChannelHandlerContext nettyContext, HttpRequest req, User user) {
        int maxFrameLength = wsConfig.getInt("maxFrameLength");
        int lo = wsConfig.getConfig("writeBufferWaterMark").getInt("low");
        int hi = wsConfig.getConfig("writeBufferWaterMark").getInt("high");
        var waterMark = new WriteBufferWaterMark(lo, hi);

        var pipeline = nettyContext.pipeline();
        pipeline.addLast(new HttpObjectAggregator(65536));
        pipeline.addLast(new WebSocketFrameDropper(waterMark.high()));
        pipeline.addLast(new WebSocketServerCompressionHandler());

        // Add websocket-specific handlers to channel pipeline
        String webSocketPath = req.uri();
        String subprotocols = "json, protobuf";
        pipeline.addLast(new WebSocketServerProtocolHandler(webSocketPath, subprotocols, true, maxFrameLength));

        // Emit idle events (interpreted by WebSocketFrameHandler).
        // Useful for avoiding unexpected closes when there's no activity.
        var pingWhenIdleFor = wsConfig.getInt("pingWhenIdleFor");
        if (pingWhenIdleFor > 0) {
            pipeline.addLast(new IdleStateHandler(0, 0, pingWhenIdleFor));
        }

        pipeline.addLast(new WebSocketFrameHandler(httpServer, req, user, waterMark));

        // Effectively trigger websocket-handler (will attempt handshake)
        nettyContext.fireChannelRead(req);
    }

    private RouteMatch matchRoute(HttpMethod method, String uri) throws MethodNotAllowedException {
        for (Route route : httpServer.getRoutes()) {
            if (route.getHttpMethod().equals(method)) {
                Matcher matcher = route.matchURI(uri);
                if (matcher.matches()) {
                    if (route.isDeprecated()) {
                        log.warn("A client used a deprecated route: {}", uri);
                    }

                    return new RouteMatch(matcher, route);
                }
            }
        }

        // Second pass, in case we did not find an exact match
        Set allowedMethods = new HashSet<>(4);
        for (Route route : httpServer.getRoutes()) {
            Matcher matcher = route.matchURI(uri);
            if (matcher.matches()) {
                allowedMethods.add(method);
            }
        }
        if (!allowedMethods.isEmpty()) {
            throw new MethodNotAllowedException(method, uri, allowedMethods);
        }

        return null;
    }

    /**
     * Represents a matched route pattern
     */
    private static final class RouteMatch {
        final Matcher regexMatch;
        final Route route;

        RouteMatch(Matcher regexMatch, Route route) {
            this.regexMatch = regexMatch;
            this.route = route;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy