caddy(3) 為 caddy 添加一個(gè) 反向代理插件

caddy-grpc 為 caddy 添加一個(gè) 反向代理插件

項(xiàng)目地址:https://github.com/yhyddr/caddy-grpc


<a name="A17eq"></a>

前言

上一次我們學(xué)習(xí)了如何在 Caddy 中擴(kuò)展自己想要的插件。博客中只提供了大致框架。這一次,我們來根據(jù)具體插件 caddy-grpc 學(xué)習(xí)。

選取它的原因是,它本身是一個(gè)獨(dú)立的應(yīng)用,這里把它做成了一個(gè) Caddy 的插件。或許你有進(jìn)一步理解到 Caddy 的良好設(shè)計(jì)。
<a name="dKJp3"></a>

插件作用

該插件的目的與Improbable-eng/grpc-web/go/grpcwebproxy目的相同,但作為 Caddy 中間件插件而不是獨(dú)立的Go應(yīng)用程序。

而這個(gè)項(xiàng)目的作用又是什么呢?

這是一個(gè)小型反向代理,可以使用gRPC-Web協(xié)議支持現(xiàn)有的gRPC服務(wù)器并公開其功能,允許從瀏覽器中使用gRPC服務(wù)。
特征:

  • 結(jié)構(gòu)化記錄(就是 log 啦)代理請求到stdout(標(biāo)準(zhǔn)輸出)
  • 可調(diào)試的 HTTP 端口(默認(rèn)端口8080
  • Prometheus監(jiān)視代理請求(/metrics在調(diào)試端點(diǎn)上)
  • Request(/debug/requests)和連接跟蹤端點(diǎn)(/debug/events
  • TLS 1.2服務(wù)(默認(rèn)端口8443):
    • 具有啟用客戶端證書驗(yàn)證的選項(xiàng)
  • 安全(純文本)和TLS gRPC后端連接:
    • 使用可自定義的CA證書進(jìn)行連接

其實(shí)意思就是,把這一個(gè)反向代理做到了 caddy 服務(wù)器的中間件中。

<a name="Jazp2"></a>

使用

在你需要的時(shí)候,可以通過

example.com 
grpc localhost:9090

第一行example.com是要服務(wù)的站點(diǎn)的主機(jī)名/地址。 第二行是一個(gè)名為grpc的指令,其中可以指定后端gRPC服務(wù)端點(diǎn)地址(即示例中的localhost:9090)。 (注意:以上配置默認(rèn)為TLS 1.2到后端gRPC服務(wù))

<a name="ZVQkt"></a>

Caddyfile 語法

grpc backend_addr {
    backend_is_insecure 
    backend_tls_noverify
    backend_tls_ca_files path_to_ca_file1 path_to_ca_file2 
}

<a name="aHpQT"></a>

backend_is_insecure

默認(rèn)情況下,代理將使用TLS連接到后端,但是如果后端以明文形式提供服務(wù),則需要添加此選項(xiàng)
<a name="7c24Y"></a>

backend_tls_noverify

默認(rèn)情況下,要驗(yàn)證后端的TLS。如果不要驗(yàn)證,則需要添加此選項(xiàng)
<a name="VzdVC"></a>

backend_tls_ca_files

用于驗(yàn)證后端證書的PEM證書鏈路徑(以逗號(hào)分隔)。 如果為空,將使用 host 主機(jī)CA鏈。
<a name="SeEBQ"></a>

<a name="dDLHd"></a>

源碼

<a name="Io2Kz"></a>

目錄結(jié)構(gòu)

caddy-grpc
├── LICENSE
├── README.md
├── proxy // 代理 grpc proxy 的功能實(shí)現(xiàn)
│   ├── DOC.md
│   ├── LICENSE.txt
│   ├── README.md
│   ├── codec.go
│   ├── director.go
│   ├── doc.go
│   └── handler.go
├── server.go // Handle 邏輯文件
└── setup.go // 安裝文件

<a name="AVXjN"></a>

Setup.go

按照我們上次進(jìn)行的 插件編寫的順序來看,如果不記得,請看:如何為 caddy 添加插件擴(kuò)展

首先看 安裝的 setup.go 文件
<a name="cnl4W"></a>

init func

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

可以知道,該插件 注冊的 是 http 服務(wù)器,名字叫 grpc

<a name="JCVyC"></a>

setup func

然后我們看到最重要的 setup 函數(shù),剛才提到的使用方法中,負(fù)責(zé)分析 caddyfile 中的選項(xiàng)的正是它。它也會(huì)將分析到的 directive 交由 Caddy 的 controller 來配置自己這個(gè)插件

// 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() 起手,用來讀取配置文件,實(shí)際上這里,它讀取了 grpc 這個(gè) token 并進(jìn)行下一步

  2. 然后我們看到,緊跟著 grpc 讀取的是 監(jiān)聽地址。

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

這里正好對(duì)應(yīng) 在 caddyfile 中的配置 grpc localhost:9090

  1. 注意 c.Next(), c.Args(), c.NextBlock(), 都是讀取 caddyfile 中的配置的函數(shù),在caddy 中我們稱為 token
  1. 另外是注意到 tls 的配置,前面有提到,該服務(wù)是開啟 tls 1.2 的服務(wù)的
        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() 來進(jìn)行每一個(gè)新 token 的分析,使用 c.Val() 讀取之后進(jìn)行不同的配置。

  1. 最后,別忘了我們要把它加入 整個(gè) caddy 的中間件中去
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
            s.next = next
            return s
        })

<a name="KdOgQ"></a>

server.go

下面進(jìn)行第二步。
<a name="7mIH9"></a>

struct

首先查看這一個(gè)插件最核心的結(jié)構(gòu)。即存儲(chǔ)了哪些數(shù)據(jù)

type server struct {
    backendAddr       string
    next              httpserver.Handler
    backendIsInsecure bool
    backendTLS        *tls.Config
    wrappedGrpc       *grpcweb.WrappedGrpcServer
}
  • backendAddr 是 grpc 服務(wù)的監(jiān)聽地址
  • next 是下一個(gè)插件的 Handler 的處理
  • backendIsInsecure 和 backendTLS 都是后臺(tái)服務(wù)是否啟用了不同的安全策略。
  • wrappedGrpc 是這個(gè)插件的關(guān)鍵,它實(shí)現(xiàn)的是 grpcweb protocol,來讓 grpc 服務(wù)能夠被瀏覽器訪問。

<a name="dQzVc"></a>

serveHTTP

我們上次的文章中,這是第二重要的部分, serveHTTP 的實(shí)現(xiàn)代表著具體的功能。上一次我們的內(nèi)容只有用來傳遞給下一個(gè) Handle 的邏輯

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

現(xiàn)在我們來看 這個(gè) 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 ,你就會(huì)知道這是用來配置 grpc 客戶端的選項(xiàng)。這里為我們的客戶端增添了 Codec 編解碼和不同的安全策略選項(xiàng)。
    //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)
    }
  • 然后是設(shè)置了 grpc 服務(wù)器的選項(xiàng)
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 來實(shí)現(xiàn) web 服務(wù)的調(diào)用
// gRPC-Web compatibility layer with CORS configured to accept on every
    wrappedGrpc := grpcweb.WrapServer(grpcServer, grpcweb.WithCorsForRegisteredEndpointsOnly(false))
    wrappedGrpc.ServeHTTP(w, r)

<a name="DO4Sy"></a>

Proxy

注意到,在上文中使用了 proxy.TransparentHandler 這是在 proxy 的 handler.go 中定義的函數(shù)。用來實(shí)現(xiàn) gRPC 服務(wù)的代理。這里涉及到 關(guān)于 gRPC 的交互的實(shí)現(xiàn),重點(diǎn)是 Client 和 Server 的 stream 傳輸,與本文關(guān)系不大,有興趣可以下來了解。

<a name="omihz"></a>

結(jié)語

思考一下把這個(gè)作為 Caddy 的插件帶來了什么?

是不是一瞬間獲得了很多可以擴(kuò)展的配置?<br />而不是將 Caddy 中想要的一些插件的功能做到 最開始說的那個(gè)獨(dú)立應(yīng)用的項(xiàng)目中。

如果你也在做 HTTP 服務(wù),還在眼饞 Caddy 中的一些功能和它的生態(tài),就像這樣接入吧。

它還涉及到了 grpc-web ,如果有興趣,可以擴(kuò)展學(xué)習(xí)一下
<a name="AhCtr"></a>

grpc-web client implementations/examples:

Vue.js<br />GopherJS

<a name="RfaYd"></a>

參考

caddy:https://github.com/caddyserver/caddy<br />如何寫中間件:https://github.com/caddyserver/caddy/wiki/Writing-a-Plugin:-HTTP-Middleware<br />caddy-grpc插件:https://github.com/pieterlouw/caddy-grpc

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容