mirror of
https://github.com/taigrr/wasm-experiments
synced 2025-01-18 04:03:21 -08:00
205 lines
7.6 KiB
Go
205 lines
7.6 KiB
Go
//Copyright 2017 Improbable. All Rights Reserved.
|
|
// See LICENSE for licensing terms.
|
|
|
|
package grpcweb
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
"github.com/rs/cors"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/grpclog"
|
|
)
|
|
|
|
var (
|
|
internalRequestHeadersWhitelist = []string{
|
|
"U-A", // for gRPC-Web User Agent indicator.
|
|
}
|
|
)
|
|
|
|
type WrappedGrpcServer struct {
|
|
server *grpc.Server
|
|
opts *options
|
|
corsWrapper *cors.Cors
|
|
originFunc func(origin string) bool
|
|
enableWebsockets bool
|
|
websocketOriginFunc func(req *http.Request) bool
|
|
}
|
|
|
|
// WrapServer takes a gRPC Server in Go and returns a WrappedGrpcServer that provides gRPC-Web Compatibility.
|
|
//
|
|
// The internal implementation fakes out a http.Request that carries standard gRPC, and performs the remapping inside
|
|
// http.ResponseWriter, i.e. mostly the re-encoding of Trailers (that carry gRPC status).
|
|
//
|
|
// You can control the behaviour of the wrapper (e.g. modifying CORS behaviour) using `With*` options.
|
|
func WrapServer(server *grpc.Server, options ...Option) *WrappedGrpcServer {
|
|
opts := evaluateOptions(options)
|
|
corsWrapper := cors.New(cors.Options{
|
|
AllowOriginFunc: opts.originFunc,
|
|
AllowedHeaders: append(opts.allowedRequestHeaders, internalRequestHeadersWhitelist...),
|
|
ExposedHeaders: nil, // make sure that this is *nil*, otherwise the WebResponse overwrite will not work.
|
|
AllowCredentials: true, // always allow credentials, otherwise :authorization headers won't work
|
|
MaxAge: int(10 * time.Minute / time.Second), // make sure pre-flights don't happen too often (every 5s for Chromium :( )
|
|
})
|
|
websocketOriginFunc := opts.websocketOriginFunc
|
|
if websocketOriginFunc == nil {
|
|
websocketOriginFunc = func(req *http.Request) bool {
|
|
origin := req.Header.Get("Origin")
|
|
parsedUrl, err := url.ParseRequestURI(origin)
|
|
if err != nil {
|
|
grpclog.Warningf("Unable to parse url for grpc-websocket origin check: %s. error: %v", origin, err)
|
|
return false
|
|
}
|
|
return parsedUrl.Host == req.Host
|
|
}
|
|
}
|
|
return &WrappedGrpcServer{
|
|
server: server,
|
|
opts: opts,
|
|
corsWrapper: corsWrapper,
|
|
originFunc: opts.originFunc,
|
|
enableWebsockets: opts.enableWebsockets,
|
|
websocketOriginFunc: websocketOriginFunc,
|
|
}
|
|
}
|
|
|
|
// ServeHTTP takes a HTTP request and if it is a gRPC-Web request wraps it with a compatibility layer to transform it to
|
|
// a standard gRPC request for the wrapped gRPC server and transforms the response to comply with the gRPC-Web protocol.
|
|
//
|
|
// The gRPC-Web compatibility is only invoked if the request is a gRPC-Web request as determined by IsGrpcWebRequest or
|
|
// the request is a pre-flight (CORS) request as determined by IsAcceptableGrpcCorsRequest.
|
|
//
|
|
// You can control the CORS behaviour using `With*` options in the WrapServer function.
|
|
func (w *WrappedGrpcServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
|
if w.enableWebsockets && w.IsGrpcWebSocketRequest(req) {
|
|
if w.websocketOriginFunc(req) {
|
|
if !w.opts.corsForRegisteredEndpointsOnly || w.isRequestForRegisteredEndpoint(req) {
|
|
w.HandleGrpcWebsocketRequest(resp, req)
|
|
return
|
|
}
|
|
}
|
|
resp.WriteHeader(403)
|
|
resp.Write(make([]byte, 0))
|
|
return
|
|
}
|
|
|
|
if w.IsAcceptableGrpcCorsRequest(req) || w.IsGrpcWebRequest(req) {
|
|
w.corsWrapper.Handler(http.HandlerFunc(w.HandleGrpcWebRequest)).ServeHTTP(resp, req)
|
|
return
|
|
}
|
|
w.server.ServeHTTP(resp, req)
|
|
}
|
|
|
|
// IsGrpcWebSocketRequest determines if a request is a gRPC-Web request by checking that the "Sec-Websocket-Protocol"
|
|
// header value is "grpc-websockets"
|
|
func (w *WrappedGrpcServer) IsGrpcWebSocketRequest(req *http.Request) bool {
|
|
return req.Header.Get("Upgrade") == "websocket" && req.Header.Get("Sec-Websocket-Protocol") == "grpc-websockets"
|
|
}
|
|
|
|
// HandleGrpcWebRequest takes a HTTP request that is assumed to be a gRPC-Web request and wraps it with a compatibility
|
|
// layer to transform it to a standard gRPC request for the wrapped gRPC server and transforms the response to comply
|
|
// with the gRPC-Web protocol.
|
|
func (w *WrappedGrpcServer) HandleGrpcWebRequest(resp http.ResponseWriter, req *http.Request) {
|
|
intReq := hackIntoNormalGrpcRequest(req)
|
|
intResp := newGrpcWebResponse(resp)
|
|
w.server.ServeHTTP(intResp, intReq)
|
|
intResp.finishRequest(req)
|
|
}
|
|
|
|
var websocketUpgrader = websocket.Upgrader{
|
|
ReadBufferSize: 1024,
|
|
WriteBufferSize: 1024,
|
|
CheckOrigin: func(r *http.Request) bool { return true },
|
|
Subprotocols: []string{"grpc-websockets"},
|
|
}
|
|
|
|
// HandleGrpcWebsocketRequest takes a HTTP request that is assumed to be a gRPC-Websocket request and wraps it with a
|
|
// compatibility layer to transform it to a standard gRPC request for the wrapped gRPC server and transforms the
|
|
// response to comply with the gRPC-Web protocol.
|
|
func (w *WrappedGrpcServer) HandleGrpcWebsocketRequest(resp http.ResponseWriter, req *http.Request) {
|
|
conn, err := websocketUpgrader.Upgrade(resp, req, nil)
|
|
if err != nil {
|
|
grpclog.Errorf("Unable to upgrade websocket request: %v", err)
|
|
return
|
|
}
|
|
w.handleWebSocket(conn, req)
|
|
}
|
|
|
|
func (w *WrappedGrpcServer) handleWebSocket(wsConn *websocket.Conn, req *http.Request) {
|
|
messageType, readBytes, err := wsConn.ReadMessage()
|
|
if err != nil {
|
|
grpclog.Errorf("Unable to read first websocket message: %v", err)
|
|
return
|
|
}
|
|
|
|
if messageType != websocket.BinaryMessage {
|
|
grpclog.Errorf("First websocket message is non-binary")
|
|
return
|
|
}
|
|
|
|
headers, err := parseHeaders(string(readBytes))
|
|
if err != nil {
|
|
grpclog.Errorf("Unable to parse websocket headers: %v", err)
|
|
return
|
|
}
|
|
|
|
respWriter := newWebSocketResponseWriter(wsConn)
|
|
wrappedReader := newWebsocketWrappedReader(wsConn, respWriter)
|
|
|
|
req.Body = wrappedReader
|
|
req.Method = http.MethodPost
|
|
req.Header = headers
|
|
req.ProtoMajor = 2
|
|
req.ProtoMinor = 0
|
|
contentType := req.Header.Get("content-type")
|
|
req.Header.Set("content-type", strings.Replace(contentType, "application/grpc-web", "application/grpc", 1))
|
|
|
|
w.server.ServeHTTP(respWriter, req)
|
|
}
|
|
|
|
// IsGrpcWebRequest determines if a request is a gRPC-Web request by checking that the "content-type" is
|
|
// "application/grpc-web" and that the method is POST.
|
|
func (w *WrappedGrpcServer) IsGrpcWebRequest(req *http.Request) bool {
|
|
return req.Method == http.MethodPost && strings.HasPrefix(req.Header.Get("content-type"), "application/grpc-web")
|
|
}
|
|
|
|
// IsAcceptableGrpcCorsRequest determines if a request is a CORS pre-flight request for a gRPC-Web request and that this
|
|
// request is acceptable for CORS.
|
|
//
|
|
// You can control the CORS behaviour using `With*` options in the WrapServer function.
|
|
func (w *WrappedGrpcServer) IsAcceptableGrpcCorsRequest(req *http.Request) bool {
|
|
accessControlHeaders := strings.ToLower(req.Header.Get("Access-Control-Request-Headers"))
|
|
if req.Method == http.MethodOptions && strings.Contains(accessControlHeaders, "x-grpc-web") {
|
|
if w.opts.corsForRegisteredEndpointsOnly {
|
|
return w.isRequestForRegisteredEndpoint(req)
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (w *WrappedGrpcServer) isRequestForRegisteredEndpoint(req *http.Request) bool {
|
|
registeredEndpoints := ListGRPCResources(w.server)
|
|
requestedEndpoint := req.URL.Path
|
|
for _, v := range registeredEndpoints {
|
|
if v == requestedEndpoint {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func hackIntoNormalGrpcRequest(req *http.Request) *http.Request {
|
|
// Hack, this should be a shallow copy, but let's see if this works
|
|
req.ProtoMajor = 2
|
|
req.ProtoMinor = 0
|
|
contentType := req.Header.Get("content-type")
|
|
req.Header.Set("content-type", strings.Replace(contentType, "application/grpc-web", "application/grpc", 1))
|
|
return req
|
|
}
|