1//Copyright 2017 Improbable. All Rights Reserved.
2// See LICENSE for licensing terms.
3
4package grpcweb
5
6import (
7	"net/http"
8	"time"
9)
10
11var (
12	defaultOptions = &options{
13		allowedRequestHeaders:          []string{"*"},
14		corsForRegisteredEndpointsOnly: true,
15		originFunc:                     func(origin string) bool { return false },
16		allowNonRootResources:          false,
17	}
18)
19
20type options struct {
21	allowedRequestHeaders          []string
22	corsForRegisteredEndpointsOnly bool
23	originFunc                     func(origin string) bool
24	enableWebsockets               bool
25	websocketPingInterval          time.Duration
26	websocketOriginFunc            func(req *http.Request) bool
27	allowNonRootResources          bool
28	endpointsFunc                  *func() []string
29}
30
31func evaluateOptions(opts []Option) *options {
32	optCopy := &options{}
33	*optCopy = *defaultOptions
34	for _, o := range opts {
35		o(optCopy)
36	}
37	return optCopy
38}
39
40type Option func(*options)
41
42// WithOriginFunc allows for customizing what CORS Origin requests are allowed.
43//
44// This is controlling the CORS pre-flight `Access-Control-Allow-Origin`. This mechanism allows you to limit the
45// availability of the APIs based on the domain name of the calling website (Origin). You can provide a function that
46// filters the allowed Origin values.
47//
48// The default behaviour is to deny all requests from remote origins.
49//
50// The relevant CORS pre-flight docs:
51// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
52func WithOriginFunc(originFunc func(origin string) bool) Option {
53	return func(o *options) {
54		o.originFunc = originFunc
55	}
56}
57
58// WithCorsForRegisteredEndpointsOnly allows for customizing whether OPTIONS requests with the `X-GRPC-WEB` header will
59// only be accepted if they match a registered gRPC endpoint.
60//
61// This should be set to false to allow handling gRPC requests for unknown endpoints (e.g. for proxying).
62//
63// The default behaviour is `true`, i.e. only allows CORS requests for registered endpoints.
64func WithCorsForRegisteredEndpointsOnly(onlyRegistered bool) Option {
65	return func(o *options) {
66		o.corsForRegisteredEndpointsOnly = onlyRegistered
67	}
68}
69
70// WithEndpointsFunc allows for providing a custom function that provides all supported endpoints for use when the
71// when `WithCorsForRegisteredEndpoints` option` is not set to false (i.e. the default state).
72//
73// When wrapping a http.Handler with `WrapHttpHandler`, failing to specify the `WithEndpointsFunc` option will cause
74// all CORS requests to result in a 403 error for websocket requests (if websockets are enabled) or be passed to the
75// handler http.Handler or grpc.Server backend (i.e. as if it wasn't wrapped).
76//
77// When wrapping grpc.Server with `WrapGrpcServer`, registered endpoints will be automatically detected, however if this
78// `WithEndpointsFunc` option is specified, the server will not be queried for its endpoints and this function will
79// be called instead.
80func WithEndpointsFunc(endpointsFunc func() []string) Option {
81	return func(o *options) {
82		o.endpointsFunc = &endpointsFunc
83	}
84}
85
86// WithAllowedRequestHeaders allows for customizing what gRPC request headers a browser can add.
87//
88// This is controlling the CORS pre-flight `Access-Control-Allow-Headers` method and applies to *all* gRPC handlers.
89// However, a special `*` value can be passed in that allows
90// the browser client to provide *any* header, by explicitly whitelisting all `Access-Control-Request-Headers` of the
91// pre-flight request.
92//
93// The default behaviour is `[]string{'*'}`, allowing all browser client headers. This option overrides that default,
94// while maintaining a whitelist for gRPC-internal headers.
95//
96// Unfortunately, since the CORS pre-flight happens independently from gRPC handler execution, it is impossible to
97// automatically discover it from the gRPC handler itself.
98//
99// The relevant CORS pre-flight docs:
100// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
101func WithAllowedRequestHeaders(headers []string) Option {
102	return func(o *options) {
103		o.allowedRequestHeaders = headers
104	}
105}
106
107// WithWebsockets allows for handling grpc-web requests of websockets - enabling bidirectional requests.
108//
109// The default behaviour is false, i.e. to disallow websockets
110func WithWebsockets(enableWebsockets bool) Option {
111	return func(o *options) {
112		o.enableWebsockets = enableWebsockets
113	}
114}
115
116// WithWebsocketPingInterval enables websocket keepalive pinging with the configured timeout.
117//
118// The default behaviour is to disable websocket pinging.
119func WithWebsocketPingInterval(websocketPingInterval time.Duration) Option {
120	return func(o *options) {
121		o.websocketPingInterval = websocketPingInterval
122	}
123}
124
125// WithWebsocketOriginFunc allows for customizing the acceptance of Websocket requests - usually to check that the origin
126// is valid.
127//
128// The default behaviour is to check that the origin of the request matches the host of the request and deny all requests from remote origins.
129func WithWebsocketOriginFunc(websocketOriginFunc func(req *http.Request) bool) Option {
130	return func(o *options) {
131		o.websocketOriginFunc = websocketOriginFunc
132	}
133}
134
135// WithAllowNonRootResource enables the gRPC wrapper to serve requests that have a path prefix
136// added to the URL, before the service name and method placeholders.
137//
138// This should be set to false when exposing the endpoint as the root resource, to avoid
139// the performance cost of path processing for every request.
140//
141// The default behaviour is `false`, i.e. always serves requests assuming there is no prefix to the gRPC endpoint.
142func WithAllowNonRootResource(allowNonRootResources bool) Option {
143	return func(o *options) {
144		o.allowNonRootResources = allowNonRootResources
145	}
146}
147