Всем привет) Те кому нравится использовать GRPC скорее всего с этой библиотекой уже знакомы. Это protoc plugin который читает *.proto файлы и генерит обратный прокси сервер который принимает HTTP и транслирует их в GRPC. Довольно полезная штука, когда у нас есть сервер к которому можно ходить как по GRPC так и HTTP.

Но при использовании данного плагина я понял что невозможно нормально использовать middleware. Самым проблемным в этом плане оказалась JWT авторизация, ибо я хотел бы валидировать JWT токен, и после этого ID юзера запихивать в context запроса, чтобы на каждом шаге запроса знать кто именно делает данный запрос)

При создании роутера c помощью grpc-gateway есть возможность задать некоторые опции, выглядит это примерно так:

import "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"

g := runtime.NewServeMux(
  runtime.WithMetadata(),
  runtime.WithForwardResponseOption(response.HTTPResponseModifier),
)

В опции сюда можно прокинуть довольно много всего, но нет возможности прокинуть middleware, которая может модифицировать context и при этом может вернуть ошибку.

После создания роутера надо зарегистрировать все методы которые объявлены в .proto файле. Для этого используется сгенерированная с помощью grpc-gateway функция. Пример использования этого метода:

err = sdk.RegisterTestHandlerServer(ctx, g, handlers.grpcHandler)
if err != nil {
  return nil, fmt.Errorf("error while initing handlers %w", err)
}

Я решил модифицировать код, который генерирует grpc-gateway, для того чтобы иметь возможность прокинуть middleware внутрь функции RegisterTestHandlerServer.

Middleware у меня будет иметь следующий интерфейс:

func(
  ctx context.Context, // тут можно модифицировать контекст
  req interface{}, 
  info *UnaryServerInfo, // отсюда мы можем узнать название вызванного метода
  handler UnaryHandler,
) 
(resp interface{}, err error)

Те кто шарят уже наверное поняли что это интерфейс UnaryServerInterceptor. Я решил использовать этот интерфейс по причине того, что его можно будет юзать как для GRPC запросов, так и для HTTP запросов. То есть можно написать один middleware и использовать его как для GRPC так и для HTTP вызовов.

Для того чтобы вносить изменения в код который сгенерировал grpc-gateway я решил написать protoc плагин, который будет проходиться по *.pb.gw.go файлам(это файлы которые генерит grpc-gateway) и вносить изменения в функцию Register<ServiceName>HandlerServer

  • Нам надо добавить тип UnaryServerInterceptor в аргументы функции.

  • Далее нам надо где-то до вызова самого метода вызвать middleware.

Сама функция Register<ServiceName>HandlerServer выглядит примерно так:

func RegisterTestServiceHandlerServer(
  ctx context.Context,
  mux *runtime.ServeMux,
  server TestServiceServer // я добавлю middleware после аргумента server
) error {
	mux.Handle("POST", pattern_TestService_MethodOne_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
		ctx, cancel := context.WithCancel(req.Context())
		defer cancel()
		var stream runtime.ServerTransportStream
		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
		var err error
		var annotatedContext context.Context
		annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/sdk.TestService/MethodOne", runtime.WithHTTPPathPattern("/sdk.TestService/MethodOne"))
		if err != nil {
			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
			return
		}

		// внутри функции local_request_TestService_MethodOne_0 вызывается уже сама
        // бизнес логика, поэтому нам надо вызвать интерсептор до этого момента
		resp, md, err := local_request_TestService_MethodOne_0(
          annotatedContext,
          inboundMarshaler,
          server,
          req,
          pathParams,
        )

		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
		if err != nil {
			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
			return
		}

		forward_TestService_MethodOne_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)

	})
	return nil
}

После прохода написанного мной .proto платина я хочу увидеть что то такое:

func RegisterTestServiceHandlerServer(
  ctx context.Context,
  mux *runtime.ServeMux,
  server TestServiceServer,
  interceptor grpc.UnaryServerInterceptor, // вот наша миддлваря
) error {
	mux.Handle("POST", pattern_TestService_MethodOne_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
		ctx, cancel := context.WithCancel(req.Context())
		defer cancel()
		var stream runtime.ServerTransportStream
		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
		var err error
		var annotatedContext context.Context
		annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/sdk.TestService/MethodOne", runtime.WithHTTPPathPattern("/sdk.TestService/MethodOne"))
		if err != nil {
			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
			return
		}

        // теперь тут новая функция, которая принимает внутрь себя интерсептор
        md, resp, err := interceptor_local_request_TestService_MethodOne_0(
          annotatedContext,
          inboundMarshaler,
          server,
          interceptor,
          req,
          pathParams,
        )

		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
		if err != nil {
			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
			return
		}

		forward_TestService_MethodOne_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)

	})
	return nil
}


// данная функция затевалась всего лишь как обертка для 
// local_request_TestService_MethodOne_0 которая внутри себя вызывает
// инетресептор

func interceptor_local_request_TestService_MethodOne_0(
  annotatedContext context.Context,
  inboundMarshaler runtime.Marshaler,
  server UsersServiceServer,
  interceptor grpc.UnaryServerInterceptor,
  req *http.Request,
  pathParams map[string]string) (md runtime.ServerMetadata, resp proto.Message, err error) {
  
	type handlerResponse struct {
		md   runtime.ServerMetadata
		resp proto.Message
	}

	handler := func(ctx context.Context, req any) (any, error) {
		if req, ok := req.(*http.Request); ok {
			resp, md, err := local_request_TestService_MethodOne_0(annotatedContext, inboundMarshaler, server, req, pathParams)
			return handlerResponse{resp: resp, md: md}, err
		}
		return nil, fmt.Errorf("error converting req to *http.Request")
	}

	var handlerResponseItem any

    // если интерсептор будет равен nil тогда выполняем все без него
	if interceptor == nil {
		handlerResponseItem, err = handler(annotatedContext, req)
	} else {
		handlerResponseItem, err = interceptor(annotatedContext, req, &grpc.UnaryServerInfo{Server: server, FullMethod: "/sdk.UsersService/GetRetoolUsersList"}, handler)
	}
	if err != nil {
		return
	}

	data, ok := handlerResponseItem.(handlerResponse)
	if !ok {
		return
	}

	return data.md, data.resp, nil
}

После того как плагин поправит код, можно будет сделать так:

err = sdk.RegisterTestHandlerServer(ctx, g, handlers.grpcHandler, middleware)
if err != nil {
  return nil, fmt.Errorf("error while initing handlers %w", err)
}

Теперь мы можем прокинуть middleware в RegisterTestHandlerServer и мы довольны)))

В следующей статье я могу могу рассказать подробнее как можно модифицировать код с помощью acl.

Код моего платина находится тут:
https://github.com/tarmalonchik/protoc-gen-interceptors

Комментарии (0)