V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
abser
V2EX  ›  Go 编程语言

caddy & grpc(3)为 caddy 添加一个 反向代理插件

  •  
  •   abser ·
    abserari · 2019-08-10 17:10:22 +08:00 · 2428 次点击
    这是一个创建于 1692 天前的主题,其中的信息可能已经有所发展或是发生改变。

    caddy-grpc 为 caddy 添加一个 反向代理插件

    项目地址:https://github.com/yhyddr/caddy-grpc


    前言

    上一次我们学习了如何在 Caddy 中扩展自己想要的插件。博客中只提供了大致框架。这一次,我们来根据具体插件 caddy-grpc 学习。

    选取它的原因是,它本身是一个独立的应用,这里把它做成了一个 Caddy 的插件。或许你有进一步理解到 Caddy 的良好设计。

    插件作用

    该插件的目的与Improbable-eng/grpc-web/go/grpcwebproxy目的相同,但作为 Caddy 中间件插件而不是独立的 Go 应用程序。

    而这个项目的作用又是什么呢?

    这是一个小型反向代理,可以使用 gRPC-Web 协议支持现有的 gRPC 服务器并公开其功能,允许从浏览器中使用 gRPC 服务。 特征:

    • 结构化记录(就是 log 啦)代理请求到 stdout (标准输出)
    • 可调试的 HTTP 端口(默认端口8080
    • Prometheus 监视代理请求(/metrics在调试端点上)
    • Request (/debug/requests)和连接跟踪端点(/debug/events
    • TLS 1.2 服务(默认端口8443):
      • 具有启用客户端证书验证的选项
    • 安全(纯文本)和 TLS gRPC 后端连接:
      • 使用可自定义的 CA 证书进行连接

    其实意思就是,把这一个反向代理做到了 caddy 服务器的中间件中。

    使用

    在你需要的时候,可以通过

    example.com 
    grpc localhost:9090
    

    第一行 example.com 是要服务的站点的主机名 /地址。 第二行是一个名为 grpc 的指令,其中可以指定后端 gRPC 服务端点地址(即示例中的 localhost:9090 )。 (注意:以上配置默认为 TLS 1.2 到后端 gRPC 服务)

    Caddyfile 语法

    grpc backend_addr {
        backend_is_insecure 
        backend_tls_noverify
        backend_tls_ca_files path_to_ca_file1 path_to_ca_file2 
    }
    

    backend_is_insecure

    默认情况下,代理将使用 TLS 连接到后端,但是如果后端以明文形式提供服务,则需要添加此选项

    backend_tls_noverify

    默认情况下,要验证后端的 TLS。如果不要验证,则需要添加此选项

    backend_tls_ca_files

    用于验证后端证书的 PEM 证书链路径(以逗号分隔)。 如果为空,将使用 host 主机 CA 链。

    源码

    目录结构

    caddy-grpc
    ├── LICENSE
    ├── README.md
    ├── proxy // 代理 grpc proxy 的功能实现
    │   ├── DOC.md
    │   ├── LICENSE.txt
    │   ├── README.md
    │   ├── codec.go
    │   ├── director.go
    │   ├── doc.go
    │   └── handler.go
    ├── server.go // Handle 逻辑文件
    └── setup.go // 安装文件
    

    Setup.go

    按照我们上次进行的 插件编写的顺序来看,如果不记得,请看:如何为 caddy 添加插件扩展

    首先看 安装的 setup.go 文件

    init func

    func init() {
    	caddy.RegisterPlugin("grpc", caddy.Plugin{
    		ServerType: "http",
    		Action:     setup,
    	})
    }
    

    可以知道,该插件 注册的 是 http 服务器,名字叫 grpc

    setup func

    然后我们看到最重要的 setup 函数,刚才提到的使用方法中,负责分析 caddyfile 中的选项的正是它。它也会将分析到的 directive 交由 Caddy 的 controller 来配置自己这个插件

    // setup configures a new server middleware instance.
    func setup(c *caddy.Controller) error {
    	for c.Next() {
    		var s server
    
    		if !c.Args(&s.backendAddr) { //loads next argument into backendAddr and fail if none specified
    			return c.ArgErr()
    		}
    
    		tlsConfig := &tls.Config{}
    		tlsConfig.MinVersion = tls.VersionTLS12
    
    		s.backendTLS = tlsConfig
    		s.backendIsInsecure = false
    
    		//check for more settings in Caddyfile
    		for c.NextBlock() {
    			switch c.Val() {
    			case "backend_is_insecure":
    				s.backendIsInsecure = true
    			case "backend_tls_noverify":
    				s.backendTLS = buildBackendTLSNoVerify()
    			case "backend_tls_ca_files":
    				t, err := buildBackendTLSFromCAFiles(c.RemainingArgs())
    				if err != nil {
    					return err
    				}
    				s.backendTLS = t
    			default:
    				return c.Errf("unknown property '%s'", c.Val())
    			}
    		}
    
    		httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
    			s.next = next
    			return s
    		})
    
    	}
    
    	return nil
    }
    
    1. 我们注意到 依旧是 c.Next() 起手,用来读取配置文件,实际上这里,它读取了 grpc 这个 token 并进行下一步

    2. 然后我们看到,紧跟着 grpc 读取的是 监听地址。

    if !c.Args(&s.backendAddr) { //loads next argument into backendAddr and fail if none specified
    			return c.ArgErr()
    		}
    

    这里正好对应 在 caddyfile 中的配置 grpc localhost:9090

    1. 注意 c.Next(), c.Args(), c.NextBlock(),  都是读取 caddyfile 中的配置的函数,在 caddy 中我们称为 token

    2. 另外是注意到 tls 的配置,前面有提到,该服务是开启 tls 1.2 的服务的

    		tlsConfig := &tls.Config{}
    		tlsConfig.MinVersion = tls.VersionTLS12
    
    		s.backendTLS = tlsConfig
    		s.backendIsInsecure = false
    
    1. 然后是上面所说的 caddyfile 语法中的配置读取
    //check for more settings in Caddyfile
    		for c.NextBlock() {
    			switch c.Val() {
    			case "backend_is_insecure":
    				s.backendIsInsecure = true
    			case "backend_tls_noverify":
    				s.backendTLS = buildBackendTLSNoVerify()
    			case "backend_tls_ca_files":
    				t, err := buildBackendTLSFromCAFiles(c.RemainingArgs())
    				if err != nil {
    					return err
    				}
    				s.backendTLS = t
    			default:
    				return c.Errf("unknown property '%s'", c.Val())
    			}
    		}
    

    可以看到是通过 c.NextBlock() 来进行每一个新 token 的分析,使用 c.Val() 读取之后进行不同的配置。

    1. 最后,别忘了我们要把它加入 整个 caddy 的中间件中去
    httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
    			s.next = next
    			return s
    		})
    

    server.go

    下面进行第二步。

    struct

    首先查看这一个插件最核心的结构。即存储了哪些数据

    type server struct {
    	backendAddr       string
    	next              httpserver.Handler
    	backendIsInsecure bool
    	backendTLS        *tls.Config
    	wrappedGrpc       *grpcweb.WrappedGrpcServer
    }
    
    • backendAddr 是 grpc 服务的监听地址
    • next 是下一个插件的 Handler 的处理
    • backendIsInsecure 和 backendTLS 都是后台服务是否启用了不同的安全策略。
    • wrappedGrpc 是这个插件的关键,它实现的是 grpcweb protocol,来让 grpc 服务能够被浏览器访问。

    serveHTTP

    我们上次的文章中,这是第二重要的部分,serveHTTP 的实现代表着具体的功能。上一次我们的内容只有用来传递给下一个 Handle 的逻辑

    func (g gizmoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
      return g.next.ServeHTTP(w, r)
    }
    

    现在我们来看 这个 grpc 中添加了什么逻辑吧。

    // ServeHTTP satisfies the httpserver.Handler interface.
    func (s server) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
    	//dial Backend
    	opt := []grpc.DialOption{}
    	opt = append(opt, grpc.WithCodec(proxy.Codec()))
    	if s.backendIsInsecure {
    		opt = append(opt, grpc.WithInsecure())
    	} else {
    		opt = append(opt, grpc.WithTransportCredentials(credentials.NewTLS(s.backendTLS)))
    	}
    
    	backendConn, err := grpc.Dial(s.backendAddr, opt...)
    	if err != nil {
    		return s.next.ServeHTTP(w, r)
    	}
    
    	director := func(ctx context.Context, fullMethodName string) (context.Context, *grpc.ClientConn, error) {
    		md, _ := metadata.FromIncomingContext(ctx)
    		return metadata.NewOutgoingContext(ctx, md.Copy()), backendConn, nil
    	}
    	grpcServer := grpc.NewServer(
    		grpc.CustomCodec(proxy.Codec()), // needed for proxy to function.
    		grpc.UnknownServiceHandler(proxy.TransparentHandler(director)),
    		/*grpc_middleware.WithUnaryServerChain(
    			grpc_logrus.UnaryServerInterceptor(logger),
    			grpc_prometheus.UnaryServerInterceptor,
    		),
    		grpc_middleware.WithStreamServerChain(
    			grpc_logrus.StreamServerInterceptor(logger),
    			grpc_prometheus.StreamServerInterceptor,
    		),*/ //middleware should be a config setting or 3rd party middleware plugins like for caddyhttp
    	)
    
    	// gRPC-Web compatibility layer with CORS configured to accept on every
    	wrappedGrpc := grpcweb.WrapServer(grpcServer, grpcweb.WithCorsForRegisteredEndpointsOnly(false))
    	wrappedGrpc.ServeHTTP(w, r)
    
    	return 0, nil
    }
    
    
    • 首先是 grpc 的配置部分,如果你了解 grpc,你就会知道这是用来配置 grpc 客户端的选项。这里为我们的客户端增添了 Codec 编解码和不同的安全策略选项。
    	//dial Backend
    	opt := []grpc.DialOption{}
    	opt = append(opt, grpc.WithCodec(proxy.Codec()))
    	if s.backendIsInsecure {
    		opt = append(opt, grpc.WithInsecure())
    	} else {
    		opt = append(opt, grpc.WithTransportCredentials(credentials.NewTLS(s.backendTLS)))
    	}
    	backendConn, err := grpc.Dial(s.backendAddr, opt...)
    	if err != nil {
    		return s.next.ServeHTTP(w, r)
    	}
    
    • 然后是设置了 grpc 服务器的选项
    director := func(ctx context.Context, fullMethodName string) (context.Context, *grpc.ClientConn, error) {
    		md, _ := metadata.FromIncomingContext(ctx)
    		return metadata.NewOutgoingContext(ctx, md.Copy()), backendConn, nil
    	}
    	grpcServer := grpc.NewServer(
    		grpc.CustomCodec(proxy.Codec()), // needed for proxy to function.
    		grpc.UnknownServiceHandler(proxy.TransparentHandler(director)),
    		/*grpc_middleware.WithUnaryServerChain(
    			grpc_logrus.UnaryServerInterceptor(logger),
    			grpc_prometheus.UnaryServerInterceptor,
    		),
    		grpc_middleware.WithStreamServerChain(
    			grpc_logrus.StreamServerInterceptor(logger),
    			grpc_prometheus.StreamServerInterceptor,
    		),*/ //middleware should be a config setting or 3rd party middleware plugins like for caddyhttp
    	)
    
    • 最后是使用 grpcweb.WrapServer 来实现 web 服务的调用
    // gRPC-Web compatibility layer with CORS configured to accept on every
    	wrappedGrpc := grpcweb.WrapServer(grpcServer, grpcweb.WithCorsForRegisteredEndpointsOnly(false))
    	wrappedGrpc.ServeHTTP(w, r)
    

    Proxy

    注意到,在上文中使用了 proxy.TransparentHandler 这是在 proxy 的 handler.go 中定义的函数。用来实现 gRPC 服务的代理。这里涉及到 关于 gRPC 的交互的实现,重点是 Client 和 Server 的 stream 传输,与本文关系不大,有兴趣可以下来了解。

    结语

    思考一下把这个作为 Caddy 的插件带来了什么?

    是不是一瞬间获得了很多可以扩展的配置?
    而不是将 Caddy 中想要的一些插件的功能做到 最开始说的那个独立应用的项目中。

    如果你也在做 HTTP 服务,还在眼馋 Caddy 中的一些功能和它的生态,就像这样接入吧。

    它还涉及到了 grpc-web,如果有兴趣,可以扩展学习一下

    grpc-web client implementations/examples:

    Vue.js
    GopherJS

    参考

    caddy:https://github.com/caddyserver/caddy
    如何写中间件:https://github.com/caddyserver/caddy/wiki/Writing-a-Plugin:-HTTP-Middleware
    caddy-grpc 插件:https://github.com/pieterlouw/caddy-grpc

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3103 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 12:55 · PVG 20:55 · LAX 05:55 · JFK 08:55
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.