1
0
mirror of https://github.com/taigrr/wasm-experiments synced 2025-01-18 04:03:21 -08:00
2018-05-13 15:59:39 +01:00

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
}