1
0
mirror of https://github.com/taigrr/wasm-experiments synced 2025-01-18 04:03:21 -08:00

Add experimental gRPC-Web folder

This commit is contained in:
Johan Brandhorst
2018-05-13 15:59:39 +01:00
parent 8725450750
commit d73473d0f6
606 changed files with 356893 additions and 0 deletions

View File

@@ -0,0 +1,211 @@
# grpcweb
--
import "github.com/improbable-eng/grpc-web/go/grpcweb"
`grpcweb` implements the gRPC-Web spec as a wrapper around a gRPC-Go Server.
It allows web clients (see companion JS library) to talk to gRPC-Go servers over
the gRPC-Web spec. It supports HTTP/1.1 and HTTP2 encoding of a gRPC stream and
supports unary and server-side streaming RPCs. Bi-di and client streams are
unsupported due to limitations in browser protocol support.
See https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md for the
protocol specification.
Here's an example of how to use it inside an existing gRPC Go server on a
separate http.Server that serves over TLS:
grpcServer := grpc.Server()
wrappedGrpc := grpcweb.WrapServer(grpcServer)
tlsHttpServer.Handler = http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
if wrappedGrpc.IsGrpcWebRequest(req) {
wrappedGrpc.ServeHTTP(resp, req)
}
// Fall back to other servers.
http.DefaultServeMux.ServeHTTP(resp, req)
})
If you'd like to have a standalone binary, please take a look at `grpcwebproxy`.
## Usage
#### func ListGRPCResources
```go
func ListGRPCResources(server *grpc.Server) []string
```
ListGRPCResources is a helper function that lists all URLs that are registered
on gRPC server.
This makes it easy to register all the relevant routes in your HTTP router of
choice.
#### type Option
```go
type Option func(*options)
```
#### func WithAllowedRequestHeaders
```go
func WithAllowedRequestHeaders(headers []string) Option
```
WithAllowedRequestHeaders allows for customizing what gRPC request headers a
browser can add.
This is controlling the CORS pre-flight `Access-Control-Allow-Headers` method
and applies to *all* gRPC handlers. However, a special `*` value can be passed
in that allows the browser client to provide *any* header, by explicitly
whitelisting all `Access-Control-Request-Headers` of the pre-flight request.
The default behaviour is `[]string{'*'}`, allowing all browser client headers.
This option overrides that default, while maintaining a whitelist for
gRPC-internal headers.
Unfortunately, since the CORS pre-flight happens independently from gRPC handler
execution, it is impossible to automatically discover it from the gRPC handler
itself.
The relevant CORS pre-flight docs:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
#### func WithCorsForRegisteredEndpointsOnly
```go
func WithCorsForRegisteredEndpointsOnly(onlyRegistered bool) Option
```
WithCorsForRegisteredEndpointsOnly allows for customizing whether OPTIONS
requests with the `X-GRPC-WEB` header will only be accepted if they match a
registered gRPC endpoint.
This should be set to false to allow handling gRPC requests for unknown
endpoints (e.g. for proxying).
The default behaviour is `true`, i.e. only allows CORS requests for registered
endpoints.
#### func WithOriginFunc
```go
func WithOriginFunc(originFunc func(origin string) bool) Option
```
WithOriginFunc allows for customizing what CORS Origin requests are allowed.
This is controlling the CORS pre-flight `Access-Control-Allow-Origin`. This
mechanism allows you to limit the availability of the APIs based on the domain
name of the calling website (Origin). You can provide a function that filters
the allowed Origin values.
The default behaviour is `*`, i.e. to allow all calling websites.
The relevant CORS pre-flight docs:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
#### func WithWebsocketOriginFunc
```go
func WithWebsocketOriginFunc(websocketOriginFunc func(req *http.Request) bool) Option
```
WithWebsocketOriginFunc allows for customizing the acceptance of Websocket
requests - usually to check that the origin is valid.
The default behaviour is to check that the origin of the request matches the
host of the request.
#### func WithWebsockets
```go
func WithWebsockets(enableWebsockets bool) Option
```
WithWebsockets allows for handling grpc-web requests of websockets - enabling
bidirectional requests.
The default behaviour is false, i.e. to disallow websockets
#### type WrappedGrpcServer
```go
type WrappedGrpcServer struct {
}
```
#### func WrapServer
```go
func WrapServer(server *grpc.Server, options ...Option) *WrappedGrpcServer
```
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 (*WrappedGrpcServer) HandleGrpcWebRequest
```go
func (w *WrappedGrpcServer) HandleGrpcWebRequest(resp http.ResponseWriter, req *http.Request)
```
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 (*WrappedGrpcServer) HandleGrpcWebsocketRequest
```go
func (w *WrappedGrpcServer) HandleGrpcWebsocketRequest(resp http.ResponseWriter, req *http.Request)
```
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 (*WrappedGrpcServer) IsAcceptableGrpcCorsRequest
```go
func (w *WrappedGrpcServer) IsAcceptableGrpcCorsRequest(req *http.Request) bool
```
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 (*WrappedGrpcServer) IsGrpcWebRequest
```go
func (w *WrappedGrpcServer) IsGrpcWebRequest(req *http.Request) bool
```
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 (*WrappedGrpcServer) IsGrpcWebSocketRequest
```go
func (w *WrappedGrpcServer) IsGrpcWebSocketRequest(req *http.Request) bool
```
IsGrpcWebSocketRequest determines if a request is a gRPC-Web request by checking
that the "Sec-Websocket-Protocol" header value is "grpc-websockets"
#### func (*WrappedGrpcServer) ServeHTTP
```go
func (w *WrappedGrpcServer) ServeHTTP(resp http.ResponseWriter, req *http.Request)
```
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.

View File

@@ -0,0 +1 @@
DOC.md

View File

@@ -0,0 +1,28 @@
// Copyright 2017 Improbable. All Rights Reserved.
// See LICENSE for licensing terms.
/*
`grpcweb` implements the gRPC-Web spec as a wrapper around a gRPC-Go Server.
It allows web clients (see companion JS library) to talk to gRPC-Go servers over the gRPC-Web spec. It supports
HTTP/1.1 and HTTP2 encoding of a gRPC stream and supports unary and server-side streaming RPCs. Bi-di and client
streams are unsupported due to limitations in browser protocol support.
See https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md for the protocol specification.
Here's an example of how to use it inside an existing gRPC Go server on a separate http.Server that serves over TLS:
grpcServer := grpc.Server()
wrappedGrpc := grpcweb.WrapServer(grpcServer)
tlsHttpServer.Handler = http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
if wrappedGrpc.IsGrpcWebRequest(req) {
wrappedGrpc.ServeHTTP(resp, req)
}
// Fall back to other servers.
http.DefaultServeMux.ServeHTTP(resp, req)
})
If you'd like to have a standalone binary, please take a look at `grpcwebproxy`.
*/
package grpcweb

View File

@@ -0,0 +1,138 @@
//Copyright 2017 Improbable. All Rights Reserved.
// See LICENSE for licensing terms.
package grpcweb
import (
"bytes"
"encoding/binary"
"net/http"
"strings"
"golang.org/x/net/http2"
)
// grpcWebResponse implements http.ResponseWriter.
type grpcWebResponse struct {
wroteHeaders bool
wroteBody bool
headers http.Header
wrapped http.ResponseWriter
}
func newGrpcWebResponse(resp http.ResponseWriter) *grpcWebResponse {
return &grpcWebResponse{headers: make(http.Header), wrapped: resp}
}
func (w *grpcWebResponse) Header() http.Header {
return w.headers
}
func (w *grpcWebResponse) Write(b []byte) (int, error) {
w.wroteBody = true
return w.wrapped.Write(b)
}
func (w *grpcWebResponse) WriteHeader(code int) {
w.copyJustHeadersToWrapped()
w.writeCorsExposedHeaders()
w.wrapped.WriteHeader(code)
w.wroteHeaders = true
}
func (w *grpcWebResponse) Flush() {
if w.wroteHeaders || w.wroteBody {
// Work around the fact that WriteHeader and a call to Flush would have caused a 200 response.
// This is the case when there is no payload.
w.wrapped.(http.Flusher).Flush()
}
}
func (w *grpcWebResponse) CloseNotify() <-chan bool {
return w.wrapped.(http.CloseNotifier).CloseNotify()
}
func (w *grpcWebResponse) copyJustHeadersToWrapped() {
wrappedHeader := w.wrapped.Header()
for k, vv := range w.headers {
// Skip the pre-annoucement of Trailer headers. Don't add them to the response headers.
if strings.ToLower(k) == "trailer" {
continue
}
for _, v := range vv {
wrappedHeader.Add(k, v)
}
}
}
func (w *grpcWebResponse) finishRequest(req *http.Request) {
if w.wroteHeaders || w.wroteBody {
w.copyTrailersToPayload()
} else {
w.copyTrailersAndHeadersToWrapped()
}
}
func (w *grpcWebResponse) copyTrailersAndHeadersToWrapped() {
w.wroteHeaders = true
wrappedHeader := w.wrapped.Header()
for k, vv := range w.headers {
// Skip the pre-annoucement of Trailer headers. Don't add them to the response headers.
if strings.ToLower(k) == "trailer" {
continue
}
// Skip the Trailer prefix
if strings.HasPrefix(k, http2.TrailerPrefix) {
k = k[len(http2.TrailerPrefix):]
}
for _, v := range vv {
wrappedHeader.Add(k, v)
}
}
w.writeCorsExposedHeaders()
w.wrapped.WriteHeader(http.StatusOK)
w.wrapped.(http.Flusher).Flush()
}
func (w *grpcWebResponse) writeCorsExposedHeaders() {
// These cors handlers are added to the *response*, not a preflight.
knownHeaders := []string{}
for h := range w.wrapped.Header() {
knownHeaders = append(knownHeaders, http.CanonicalHeaderKey(h))
}
w.wrapped.Header().Set("Access-Control-Expose-Headers", strings.Join(knownHeaders, ", "))
}
func (w *grpcWebResponse) copyTrailersToPayload() {
trailers := w.extractTrailerHeaders()
trailerBuffer := new(bytes.Buffer)
trailers.Write(trailerBuffer)
trailerGrpcDataHeader := []byte{1 << 7, 0, 0, 0, 0} // MSB=1 indicates this is a trailer data frame.
binary.BigEndian.PutUint32(trailerGrpcDataHeader[1:5], uint32(trailerBuffer.Len()))
w.wrapped.Write(trailerGrpcDataHeader)
w.wrapped.Write(trailerBuffer.Bytes())
w.wrapped.(http.Flusher).Flush()
}
func (w *grpcWebResponse) extractTrailerHeaders() http.Header {
flushedHeaders := w.wrapped.Header()
trailerHeaders := make(http.Header)
for k, vv := range w.headers {
// Skip the pre-annoucement of Trailer headers. Don't add them to the response headers.
if strings.ToLower(k) == "trailer" {
continue
}
// Skip existing headers that were already sent.
if _, exists := flushedHeaders[k]; exists {
continue
}
// Skip the Trailer prefix
if strings.HasPrefix(k, http2.TrailerPrefix) {
k = k[len(http2.TrailerPrefix):]
}
for _, v := range vv {
trailerHeaders.Add(k, v)
}
}
return trailerHeaders
}

View File

@@ -0,0 +1,24 @@
//Copyright 2017 Improbable. All Rights Reserved.
// See LICENSE for licensing terms.
package grpcweb
import (
"fmt"
"google.golang.org/grpc"
)
// ListGRPCResources is a helper function that lists all URLs that are registered on gRPC server.
//
// This makes it easy to register all the relevant routes in your HTTP router of choice.
func ListGRPCResources(server *grpc.Server) []string {
ret := []string{}
for serviceName, serviceInfo := range server.GetServiceInfo() {
for _, methodInfo := range serviceInfo.Methods {
fullResource := fmt.Sprintf("/%s/%s", serviceName, methodInfo.Name)
ret = append(ret, fullResource)
}
}
return ret
}

View File

@@ -0,0 +1,101 @@
//Copyright 2017 Improbable. All Rights Reserved.
// See LICENSE for licensing terms.
package grpcweb
import "net/http"
var (
defaultOptions = &options{
allowedRequestHeaders: []string{"*"},
corsForRegisteredEndpointsOnly: true,
originFunc: func(origin string) bool { return true },
}
)
type options struct {
allowedRequestHeaders []string
corsForRegisteredEndpointsOnly bool
originFunc func(origin string) bool
enableWebsockets bool
websocketOriginFunc func(req *http.Request) bool
}
func evaluateOptions(opts []Option) *options {
optCopy := &options{}
*optCopy = *defaultOptions
for _, o := range opts {
o(optCopy)
}
return optCopy
}
type Option func(*options)
// WithOriginFunc allows for customizing what CORS Origin requests are allowed.
//
// This is controlling the CORS pre-flight `Access-Control-Allow-Origin`. This mechanism allows you to limit the
// availability of the APIs based on the domain name of the calling website (Origin). You can provide a function that
// filters the allowed Origin values.
//
// The default behaviour is `*`, i.e. to allow all calling websites.
//
// The relevant CORS pre-flight docs:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
func WithOriginFunc(originFunc func(origin string) bool) Option {
return func(o *options) {
o.originFunc = originFunc
}
}
// WithCorsForRegisteredEndpointsOnly allows for customizing whether OPTIONS requests with the `X-GRPC-WEB` header will
// only be accepted if they match a registered gRPC endpoint.
//
// This should be set to false to allow handling gRPC requests for unknown endpoints (e.g. for proxying).
//
// The default behaviour is `true`, i.e. only allows CORS requests for registered endpoints.
func WithCorsForRegisteredEndpointsOnly(onlyRegistered bool) Option {
return func(o *options) {
o.corsForRegisteredEndpointsOnly = onlyRegistered
}
}
// WithAllowedRequestHeaders allows for customizing what gRPC request headers a browser can add.
//
// This is controlling the CORS pre-flight `Access-Control-Allow-Headers` method and applies to *all* gRPC handlers.
// However, a special `*` value can be passed in that allows
// the browser client to provide *any* header, by explicitly whitelisting all `Access-Control-Request-Headers` of the
// pre-flight request.
//
// The default behaviour is `[]string{'*'}`, allowing all browser client headers. This option overrides that default,
// while maintaining a whitelist for gRPC-internal headers.
//
// Unfortunately, since the CORS pre-flight happens independently from gRPC handler execution, it is impossible to
// automatically discover it from the gRPC handler itself.
//
// The relevant CORS pre-flight docs:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
func WithAllowedRequestHeaders(headers []string) Option {
return func(o *options) {
o.allowedRequestHeaders = headers
}
}
// WithWebsockets allows for handling grpc-web requests of websockets - enabling bidirectional requests.
//
// The default behaviour is false, i.e. to disallow websockets
func WithWebsockets(enableWebsockets bool) Option {
return func(o *options) {
o.enableWebsockets = enableWebsockets
}
}
// WithWebsocketOriginFunc allows for customizing the acceptance of Websocket requests - usually to check that the origin
// is valid.
//
// The default behaviour is to check that the origin of the request matches the host of the request.
func WithWebsocketOriginFunc(websocketOriginFunc func(req *http.Request) bool) Option {
return func(o *options) {
o.websocketOriginFunc = websocketOriginFunc
}
}

View File

@@ -0,0 +1,216 @@
package grpcweb
import (
"bufio"
"bytes"
"encoding/binary"
"errors"
"io"
"net/http"
"net/textproto"
"strings"
"github.com/gorilla/websocket"
"golang.org/x/net/http2"
)
type webSocketResponseWriter struct {
writtenHeaders bool
wsConn *websocket.Conn
headers http.Header
flushedHeaders http.Header
closeNotifyChan chan bool
}
func newWebSocketResponseWriter(wsConn *websocket.Conn) *webSocketResponseWriter {
return &webSocketResponseWriter{
writtenHeaders: false,
headers: make(http.Header),
flushedHeaders: make(http.Header),
wsConn: wsConn,
closeNotifyChan: make(chan bool),
}
}
func (w *webSocketResponseWriter) Header() http.Header {
return w.headers
}
func (w *webSocketResponseWriter) Write(b []byte) (int, error) {
if !w.writtenHeaders {
w.WriteHeader(http.StatusOK)
}
return len(b), w.wsConn.WriteMessage(websocket.BinaryMessage, b)
}
func (w *webSocketResponseWriter) writeHeaderFrame(headers http.Header) {
headerBuffer := new(bytes.Buffer)
headers.Write(headerBuffer)
headerGrpcDataHeader := []byte{1 << 7, 0, 0, 0, 0} // MSB=1 indicates this is a header data frame.
binary.BigEndian.PutUint32(headerGrpcDataHeader[1:5], uint32(headerBuffer.Len()))
w.wsConn.WriteMessage(websocket.BinaryMessage, headerGrpcDataHeader)
w.wsConn.WriteMessage(websocket.BinaryMessage, headerBuffer.Bytes())
}
func (w *webSocketResponseWriter) copyFlushedHeaders() {
for k, vv := range w.headers {
// Skip the pre-annoucement of Trailer headers. Don't add them to the response headers.
if strings.ToLower(k) == "trailer" {
continue
}
for _, v := range vv {
w.flushedHeaders.Add(k, v)
}
}
}
func (w *webSocketResponseWriter) WriteHeader(code int) {
w.copyFlushedHeaders()
w.writtenHeaders = true
w.writeHeaderFrame(w.headers)
return
}
func (w *webSocketResponseWriter) extractTrailerHeaders() http.Header {
trailerHeaders := make(http.Header)
for k, vv := range w.headers {
// Skip the pre-annoucement of Trailer headers. Don't add them to the response headers.
if strings.ToLower(k) == "trailer" {
continue
}
// Skip existing headers that were already sent.
if _, exists := w.flushedHeaders[k]; exists {
continue
}
// Skip the Trailer prefix
if strings.HasPrefix(k, http2.TrailerPrefix) {
k = k[len(http2.TrailerPrefix):]
}
for _, v := range vv {
trailerHeaders.Add(k, v)
}
}
return trailerHeaders
}
func (w *webSocketResponseWriter) FlushTrailers() {
w.writeHeaderFrame(w.extractTrailerHeaders())
}
func (w *webSocketResponseWriter) Flush() {
// no-op
}
func (w *webSocketResponseWriter) CloseNotify() <-chan bool {
return w.closeNotifyChan
}
type webSocketWrappedReader struct {
wsConn *websocket.Conn
respWriter *webSocketResponseWriter
remainingBuffer []byte
remainingError error
}
func (w *webSocketWrappedReader) Close() error {
w.respWriter.FlushTrailers()
return w.wsConn.Close()
}
// First byte of a binary WebSocket frame is used for control flow:
// 0 = Data
// 1 = End of client send
func (w *webSocketWrappedReader) Read(p []byte) (int, error) {
// If a buffer remains from a previous WebSocket frame read then continue reading it
if w.remainingBuffer != nil {
// If the remaining buffer fits completely inside the argument slice then read all of it and return any error
// that was retained from the original call
if len(w.remainingBuffer) <= len(p) {
copy(p, w.remainingBuffer)
remainingLength := len(w.remainingBuffer)
err := w.remainingError
// Clear the remaining buffer and error so that the next read will be a read from the websocket frame,
// unless the error terminates the stream
w.remainingBuffer = nil
w.remainingError = nil
return remainingLength, err
}
// The remaining buffer doesn't fit inside the argument slice, so copy the bytes that will fit and retain the
// bytes that don't fit - don't return the remainingError as there are still bytes to be read from the frame
copy(p, w.remainingBuffer[:len(p)])
w.remainingBuffer = w.remainingBuffer[len(p):]
// Return the length of the argument slice as that was the length of the written bytes
return len(p), nil
}
// Read a whole frame from the WebSocket connection
messageType, framePayload, err := w.wsConn.ReadMessage()
if err == io.EOF || messageType == -1 {
// The client has closed the connection. Indicate to the response writer that it should close
w.respWriter.closeNotifyChan <- true
return 0, io.EOF
}
// Only Binary frames are valid
if messageType != websocket.BinaryMessage {
return 0, errors.New("websocket frame was not a binary frame")
}
// If the frame consists of only a single byte of value 1 then this indicates the client has finished sending
if len(framePayload) == 1 && framePayload[0] == 1 {
return 0, io.EOF
}
// If the frame is somehow empty then just return the error
if len(framePayload) == 0 {
return 0, err
}
// The first byte is used for control flow, so the data starts from the second byte
dataPayload := framePayload[1:]
// If the remaining buffer fits completely inside the argument slice then read all of it and return the error
if len(dataPayload) <= len(p) {
copy(p, dataPayload)
return len(dataPayload), err
}
// The data read from the frame doesn't fit inside the argument slice, so copy the bytes that fit into the argument
// slice
copy(p, dataPayload[:len(p)])
// Retain the bytes that do not fit in the argument slice
w.remainingBuffer = dataPayload[len(p):]
// Retain the error instead of returning it so that the retained bytes will be read
w.remainingError = err
// Return the length of the argument slice as that is the length of the written bytes
return len(p), nil
}
func newWebsocketWrappedReader(wsConn *websocket.Conn, respWriter *webSocketResponseWriter) *webSocketWrappedReader {
return &webSocketWrappedReader{
wsConn: wsConn,
respWriter: respWriter,
remainingBuffer: nil,
remainingError: nil,
}
}
func parseHeaders(headerString string) (http.Header, error) {
reader := bufio.NewReader(strings.NewReader(headerString + "\r\n"))
tp := textproto.NewReader(reader)
mimeHeader, err := tp.ReadMIMEHeader()
if err != nil {
return nil, err
}
// http.Header and textproto.MIMEHeader are both just a map[string][]string
return http.Header(mimeHeader), nil
}

View File

@@ -0,0 +1,204 @@
//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
}