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:
211
vendor/github.com/improbable-eng/grpc-web/go/grpcweb/DOC.md
generated
vendored
Normal file
211
vendor/github.com/improbable-eng/grpc-web/go/grpcweb/DOC.md
generated
vendored
Normal 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.
|
||||
1
vendor/github.com/improbable-eng/grpc-web/go/grpcweb/README.md
generated
vendored
Symbolic link
1
vendor/github.com/improbable-eng/grpc-web/go/grpcweb/README.md
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
DOC.md
|
||||
28
vendor/github.com/improbable-eng/grpc-web/go/grpcweb/doc.go
generated
vendored
Normal file
28
vendor/github.com/improbable-eng/grpc-web/go/grpcweb/doc.go
generated
vendored
Normal 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
|
||||
138
vendor/github.com/improbable-eng/grpc-web/go/grpcweb/grpc_web_response.go
generated
vendored
Normal file
138
vendor/github.com/improbable-eng/grpc-web/go/grpcweb/grpc_web_response.go
generated
vendored
Normal 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
|
||||
}
|
||||
24
vendor/github.com/improbable-eng/grpc-web/go/grpcweb/helpers.go
generated
vendored
Normal file
24
vendor/github.com/improbable-eng/grpc-web/go/grpcweb/helpers.go
generated
vendored
Normal 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
|
||||
}
|
||||
101
vendor/github.com/improbable-eng/grpc-web/go/grpcweb/options.go
generated
vendored
Normal file
101
vendor/github.com/improbable-eng/grpc-web/go/grpcweb/options.go
generated
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
216
vendor/github.com/improbable-eng/grpc-web/go/grpcweb/websocket_wrapper.go
generated
vendored
Normal file
216
vendor/github.com/improbable-eng/grpc-web/go/grpcweb/websocket_wrapper.go
generated
vendored
Normal 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
|
||||
}
|
||||
204
vendor/github.com/improbable-eng/grpc-web/go/grpcweb/wrapper.go
generated
vendored
Normal file
204
vendor/github.com/improbable-eng/grpc-web/go/grpcweb/wrapper.go
generated
vendored
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user