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

caddy & grpc(3) 为 caddy 添加一个 反向代理插件的更多相关文章

  1. nginx是一个反向代理的软件

    nginx只是一个反向代理的软件,和语言无关,理论上支持任何Web平台,当然http://Asp.net也不例外,http://51aspx.com就是http://Asp.net开发的,前端暴漏的是 ...

  2. 如何在Microsoft Edge浏览器中添加一个Hello World插件

    注:本文提到的代码示例下载地址> How to add a Hello World extension to Microsoft Edge Microsoft Edge 随着Win 10一起推出 ...

  3. 今天知道了一个 反向代理,是apache 的一个功能,这里记录一下

    什么事情都需要自己 去弄,记住了这句话,不要以为 别人会来帮你 在 apache 的http.conf 文件中(去掉注释) LoadModule proxy_module modules/mod_pr ...

  4. nodejs反向代理插件anyproxy安装

    目前我使用的是Anyproxy,AnyProxy .这个软件的特点是可以获取到https链接的内容.在2016年年初的时候微信公众号和微信文章开始使用https链接.并且Anyproxy可以通过修改r ...

  5. centos7安装nginx,以及使用node测试反向代理

    1.添加nginx的安装源 vi /etc/yum.repos.d/nginx.repo 2.输入下面内容,并保存退出 [nginx] name=nginx repo baseurl=http://n ...

  6. IIS tomcat共用80端口解决一个IP多个域名:使用Nginx反向代理方式使两者兼容

    环境: windows server 2003,IIS6服务器,Tomcat7服务器 域名有几个: 以下是使用IIS的域名: http://www.formuch.com/ http://www.fo ...

  7. (转)IIS tomcat共用80端口解决一个IP多个域名:使用Nginx反向代理方式使两者兼容

    from :http://www.cnblogs.com/wuyou/p/3455619.html 环境: windows server 2003,IIS6服务器,Tomcat7服务器 域名有几个: ...

  8. nginx的反向代理和负载均衡的一个总结

    之前一直觉的nginx的反向代理和负载均衡很厉害的样子,最近有机会接触了一下公司的这方面的技术,发现技术就是一张窗户纸呀,捅破了啥都明白了! 接下来先看一下nginx的反向代理: 简单的来说就是ngi ...

  9. 跟我一起使用webpack给一个开源项目添加一个运行入口

    啦啦啦啦啦不要把webpack想的很高大上就放弃了探究的想法,其实webpack特别的平易近人,就是一个工具 今天看到了一个超级美丽的项目 你可以看到各种各样的口红色号,满屏的粉色,哇哇哇哇塞,美美哒 ...

随机推荐

  1. sql锁的类型介绍:悲观锁,乐观锁,行锁,表锁,页锁,共享锁,排他锁,意向锁

    1 悲观锁,乐观锁 悲观锁:顾名思义,很悲观,就是每次拿数据的时候都认为别的线程会修改数据,所以在每次拿的时候都会给数据上锁.上锁之后,当别的线程想要拿数据时,就会阻塞,直到给数据上锁的线程将事务提交 ...

  2. Codeforces 617E:XOR and Favorite Number(莫队算法)

    http://codeforces.com/problemset/problem/617/E 题意:给出n个数,q个询问区间,问这个区间里面有多少个区间[i,j]可以使得ai^ai+1^...^aj ...

  3. scrapy基础知识之 Scrapy-Redis分布式策略:

    Scrapy-Redis分布式策略: 假设有四台电脑:Windows 10.Mac OS X.Ubuntu 16.04.CentOS 7.2,任意一台电脑都可以作为 Master端 或 Slaver端 ...

  4. NOIP 2004 虫食算题解

    问题 E: [Noip2004]虫食算 时间限制: 1 Sec  内存限制: 128 MB 题目描述 所谓虫食算,就是原先的算式中有一部分被虫子啃掉了,需要我们根据剩下的数字来判定被啃掉的字母.来看一 ...

  5. linux字符设备驱动--基本知识介绍

    一.设备驱动的分类 1.字符设备 字符设备是指那些能一个字节一个字节读取数据的设备,如LED灯.键盘.鼠标等.字符设备一般需要在驱动层实现open().close().read().write().i ...

  6. Ui自动化测试上传文件方法都在这里了

    前言 实施UI自动化测试的时候,经常会遇见上传文件的操作,那么对于上传文件你知道几种方法呢?今天我们就总结一下几种常用的上传文件的方法,并分析一下每个方法的优点和缺点以及哪种方法效率,稳定性更高 被测 ...

  7. TestNG的静态方法mock的步骤

    最近团队内部对程序中使用大量的`静态方法`,而公司要求要有sonar扫描覆盖率的,因为在大量使用静态方法的地方若不mock,则覆盖率达不到.于是网上很少的文章讲解对静态方法的mock,大多都是如何使用 ...

  8. c++小游戏——拯救公主

    #include<stdio.h> #include<ctime> #include<time.h> //suiji #include<windows.h&g ...

  9. SQLServer 问题(一)

    出现这种错误: [DBNETLIB][ConnectionOpen(Connect()).]SQL Server 不存在或拒绝访问 数据库错误 原因: 1.查看是不是没有在数据库中添加数据库服务器地址 ...

  10. spring boot入门篇

    Spring Boot[快速入门]   Spring Boot 概述 Build Anything with Spring Boot:Spring Boot is the starting point ...