/* * Copyright 2017 LINE Corporation * * LINE Corporation licenses this file to you 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.linecorp.armeria.server.grpc; import java.util.Map; import java.util.function.Function; import com.google.common.collect.ImmutableMap; import com.linecorp.armeria.common.MediaType; import com.linecorp.armeria.common.SerializationFormat; import com.linecorp.armeria.common.grpc.GrpcSerializationFormats; import com.linecorp.armeria.common.http.AggregatedHttpMessage; import com.linecorp.armeria.common.http.DefaultHttpRequest; import com.linecorp.armeria.common.http.DefaultHttpResponse; import com.linecorp.armeria.common.http.HttpData; import com.linecorp.armeria.common.http.HttpHeaderNames; import com.linecorp.armeria.common.http.HttpHeaders; import com.linecorp.armeria.common.http.HttpRequest; import com.linecorp.armeria.common.http.HttpResponse; import com.linecorp.armeria.common.http.HttpStatus; import com.linecorp.armeria.internal.grpc.ArmeriaMessageDeframer; import com.linecorp.armeria.internal.grpc.ArmeriaMessageDeframer.ByteBufOrStream; import com.linecorp.armeria.internal.grpc.ArmeriaMessageDeframer.Listener; import com.linecorp.armeria.internal.grpc.ArmeriaMessageFramer; import com.linecorp.armeria.internal.grpc.GrpcHeaderNames; import com.linecorp.armeria.internal.http.ByteBufHttpData; import com.linecorp.armeria.server.Service; import com.linecorp.armeria.server.ServiceRequestContext; import com.linecorp.armeria.server.SimpleDecoratingService; import io.grpc.MethodDescriptor; import io.grpc.MethodDescriptor.MethodType; import io.grpc.ServerMethodDefinition; import io.grpc.Status; import io.netty.buffer.ByteBuf; /** * A {@link SimpleDecoratingService} which allows {@link GrpcService} to serve requests without the framing * specified by the GRPC wire protocol. This can be useful for serving both legacy systems and GRPC clients with * the same business logic. * * <p>Limitations: * <ul> * <li>Only unary methods (single request, single response) are supported.</li> * <li> * Message compression is not supported. * {@link com.linecorp.armeria.server.http.encoding.HttpEncodingService} should be used instead for * transport level encoding. * </li> * </ul> */ class UnframedGrpcService extends SimpleDecoratingService<HttpRequest, HttpResponse> { private final Map<String, MethodDescriptor<?, ?>> methodsByName; /** * Creates a new instance that decorates the specified {@link Service}. */ UnframedGrpcService(Service<HttpRequest, HttpResponse> delegate) { super(delegate); GrpcService grpcService = delegate.as(GrpcService.class) .orElseThrow( () -> new IllegalArgumentException("Decorated service must be a GrpcService.")); methodsByName = grpcService.services() .stream() .flatMap(service -> service.getMethods().stream()) .map(ServerMethodDefinition::getMethodDescriptor) .collect(ImmutableMap.toImmutableMap(MethodDescriptor::getFullMethodName, Function.identity())); } @Override public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception { final HttpHeaders clientHeaders = req.headers(); final String contentType = clientHeaders.get(HttpHeaderNames.CONTENT_TYPE); if (contentType == null) { // All GRPC requests, whether framed or non-framed, must have content-type. If it's not sent, let // the delegate return its usual error message. return delegate().serve(ctx, req); } MediaType mediaType = MediaType.parse(contentType); for (SerializationFormat format : GrpcSerializationFormats.values()) { if (format.isAccepted(mediaType)) { // Framed request, so just delegate. return delegate().serve(ctx, req); } } String methodName = GrpcRequestUtil.determineMethod(ctx); MethodDescriptor<?, ?> method = methodName != null ? methodsByName.get(methodName) : null; if (method == null) { // Unknown method, let the delegate return a usual error. return delegate().serve(ctx, req); } DefaultHttpResponse res = new DefaultHttpResponse(); if (method.getType() != MethodType.UNARY) { res.respond(HttpStatus.BAD_REQUEST, MediaType.PLAIN_TEXT_UTF_8, "Only unary methods can be used with non-framed requests."); return res; } HttpHeaders grpcHeaders = HttpHeaders.copyOf(clientHeaders); if (mediaType.is(MediaType.PROTOBUF)) { grpcHeaders.set( HttpHeaderNames.CONTENT_TYPE, GrpcSerializationFormats.PROTO.mediaType().toString()); } else { res.respond(HttpStatus.UNSUPPORTED_MEDIA_TYPE, MediaType.PLAIN_TEXT_UTF_8, "Unsupported media type. Only application/protobuf is supported."); return res; } if (grpcHeaders.get(GrpcHeaderNames.GRPC_ENCODING) != null) { res.respond(HttpStatus.UNSUPPORTED_MEDIA_TYPE, MediaType.PLAIN_TEXT_UTF_8, "GRPC encoding is not supported for non-framed requests."); } // All clients support no encoding, and we don't support GRPC encoding for non-framed requests, so just // clear the header if it's present. grpcHeaders.remove(GrpcHeaderNames.GRPC_ACCEPT_ENCODING); req.aggregate().whenCompleteAsync( (clientRequest, t) -> { if (t != null) { res.close(t); } else { frameAndServe(ctx, grpcHeaders, clientRequest, res); } }, ctx.eventLoop()); return res; } private void frameAndServe( ServiceRequestContext ctx, HttpHeaders grpcHeaders, AggregatedHttpMessage clientRequest, DefaultHttpResponse res) { final DefaultHttpRequest grpcRequest = new DefaultHttpRequest(grpcHeaders); try (ArmeriaMessageFramer framer = new ArmeriaMessageFramer( ctx.alloc(), ArmeriaMessageFramer.NO_MAX_OUTBOUND_MESSAGE_SIZE)) { HttpData content = clientRequest.content(); ByteBuf message = ctx.alloc().buffer(content.length()); final HttpData frame; boolean success = false; try { message.writeBytes(content.array(), content.offset(), content.length()); frame = framer.writePayload(message); success = true; } finally { if (!success) { message.release(); } } grpcRequest.write(frame); grpcRequest.close(); } final HttpResponse grpcResponse; try { grpcResponse = delegate().serve(ctx, grpcRequest); } catch (Exception e) { res.close(e); return; } grpcResponse.aggregate().whenCompleteAsync( (framedResponse, t) -> { if (t != null) { res.close(t); } else { deframeAndRespond(ctx, framedResponse, res); } }, ctx.eventLoop()); } private void deframeAndRespond( ServiceRequestContext ctx, AggregatedHttpMessage grpcResponse, DefaultHttpResponse res) { HttpHeaders trailers = !grpcResponse.trailingHeaders().isEmpty() ? grpcResponse.trailingHeaders() : grpcResponse.headers(); String grpcStatusCode = trailers.get(GrpcHeaderNames.GRPC_STATUS); Status grpcStatus = Status.fromCodeValue(Integer.parseInt(grpcStatusCode)); if (!grpcStatus.getCode().equals(Status.OK.getCode())) { StringBuilder message = new StringBuilder("grpc-status: " + grpcStatusCode); String grpcMessage = trailers.get(GrpcHeaderNames.GRPC_MESSAGE); if (grpcMessage != null) { message.append(", ").append(grpcMessage); } res.respond(HttpStatus.INTERNAL_SERVER_ERROR, MediaType.PLAIN_TEXT_UTF_8, message.toString()); return; } MediaType grpcMediaType = MediaType.parse( grpcResponse.headers().get(HttpHeaderNames.CONTENT_TYPE)); HttpHeaders unframedHeaders = HttpHeaders.copyOf(grpcResponse.headers()); if (grpcMediaType.is(GrpcSerializationFormats.PROTO.mediaType())) { unframedHeaders.set(HttpHeaderNames.CONTENT_TYPE, MediaType.PROTOBUF.toString()); } try (ArmeriaMessageDeframer deframer = new ArmeriaMessageDeframer( new Listener() { @Override public void messageRead(ByteBufOrStream message) { // We know there is only one message in total, so don't bother with checking endOfStream // We also know that we don't support compression, so this is always a ByteBuffer. HttpData unframedContent = new ByteBufHttpData(message.buf(), true); res.respond(AggregatedHttpMessage.of( unframedHeaders, unframedContent)); } @Override public void endOfStream() {} }, // Max outbound message size is handled by the GrpcService, so we don't need to set it here. Integer.MAX_VALUE, ctx.alloc())) { deframer.request(1); deframer.deframe(grpcResponse.content(), true); } } }