Skip to content

Commit

Permalink
Merge pull request #35 from dongxuny/master
Browse files Browse the repository at this point in the history
Implementation of CORS interceptor for grpc-gateway
  • Loading branch information
dongxuny committed Nov 22, 2021
2 parents 802874c + 577fd15 commit 641955b
Show file tree
Hide file tree
Showing 10 changed files with 699 additions and 2 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Interceptor & bootstrapper designed for grpc. Currently, supports bellow functio
| Auth interceptor | Support [Basic Auth] and [API Key] authorization types. |
| RateLimit interceptor | Limit request rate from interceptor. |
| Timeout interceptor | Timing out request by configuration. |
| CORS interceptor | CORS interceptor for grpc-gateway. |

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
Expand Down Expand Up @@ -55,6 +56,7 @@ Interceptor & bootstrapper designed for grpc. Currently, supports bellow functio
- [Tracing](#tracing)
- [RateLimit](#ratelimit)
- [Timeout](#timeout)
- [CORS](#cors)
- [Development Status: Stable](#development-status-stable)
- [Build instruction](#build-instruction)
- [Test instruction](#test-instruction)
Expand Down Expand Up @@ -481,6 +483,17 @@ Send application metadata as header to client and GRPC Gateway.
| grpc.interceptors.timeout.paths.path | Full path | string | "" |
| grpc.interceptors.timeout.paths.timeoutMs | Timeout in milliseconds by full path | int | 5000 |

#### CORS
| name | description | type | default value |
| ------ | ------ | ------ | ------ |
| grpc.interceptors.cors.enabled | Enable cors interceptor | boolean | false |
| grpc.interceptors.cors.allowOrigins | Provide allowed origins with wildcard enabled. | []string | * |
| grpc.interceptors.cors.allowMethods | Provide allowed methods returns as response header of OPTIONS request. | []string | All http methods |
| grpc.interceptors.cors.allowHeaders | Provide allowed headers returns as response header of OPTIONS request. | []string | Headers from request |
| grpc.interceptors.cors.allowCredentials | Returns as response header of OPTIONS request. | bool | false |
| grpc.interceptors.cors.exposeHeaders | Provide exposed headers returns as response header of OPTIONS request. | []string | "" |
| grpc.interceptors.cors.maxAge | Provide max age returns as response header of OPTIONS request. | int | 0 |

## Development Status: Stable

## Build instruction
Expand Down
45 changes: 43 additions & 2 deletions boot/grpc_entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/rookie-ninja/rk-entry/entry"
"github.com/rookie-ninja/rk-grpc/boot/api/third_party/gen/v1"
"github.com/rookie-ninja/rk-grpc/interceptor/auth"
rkgrpccors "github.com/rookie-ninja/rk-grpc/interceptor/cors"
"github.com/rookie-ninja/rk-grpc/interceptor/log/zap"
"github.com/rookie-ninja/rk-grpc/interceptor/meta"
"github.com/rookie-ninja/rk-grpc/interceptor/metrics/prom"
Expand Down Expand Up @@ -171,6 +172,15 @@ type BootConfigGrpc struct {
Basic []string `yaml:"basic" json:"basic"`
ApiKey []string `yaml:"apiKey" json:"apiKey"`
} `yaml:"auth" json:"auth"`
Cors struct {
Enabled bool `yaml:"enabled" json:"enabled"`
AllowOrigins []string `yaml:"allowOrigins" json:"allowOrigins"`
AllowCredentials bool `yaml:"allowCredentials" json:"allowCredentials"`
AllowHeaders []string `yaml:"allowHeaders" json:"allowHeaders"`
AllowMethods []string `yaml:"allowMethods" json:"allowMethods"`
ExposeHeaders []string `yaml:"exposeHeaders" json:"exposeHeaders"`
MaxAge int `yaml:"maxAge" json:"maxAge"`
} `yaml:"cors" json:"cors"`
Meta struct {
Enabled bool `yaml:"enabled" json:"enabled"`
Prefix string `yaml:"prefix" json:"prefix"`
Expand Down Expand Up @@ -278,6 +288,7 @@ type GrpcEntry struct {
GwMappingFilePaths []string `json:"gwMappingFilePaths" yaml:"gwMappingFilePaths"`
GwDialOptions []grpc.DialOption `json:"-" yaml:"-"`
GwHttpToGrpcMapping map[string]*gwRule `json:"gwMapping" yaml:"gwMapping"`
gwCorsOptions []rkgrpccors.Option `json:"-" yaml:"-"`
// Utility related
SwEntry *SwEntry `json:"swEntry" yaml:"swEntry"`
TvEntry *TvEntry `json:"tvEntry" yaml:"tvEntry"`
Expand Down Expand Up @@ -576,6 +587,22 @@ func RegisterGrpcEntriesWithConfig(configFilePath string) map[string]rkentry.Ent
entry.AddStreamInterceptors(rkgrpctrace.StreamServerInterceptor(opts...))
}

// did we enabled cors interceptor?
// CORS interceptor is for grpc-gateway
if element.Interceptors.Cors.Enabled {
opts := []rkgrpccors.Option{
rkgrpccors.WithEntryNameAndType(element.Name, GrpcEntryType),
rkgrpccors.WithAllowOrigins(element.Interceptors.Cors.AllowOrigins...),
rkgrpccors.WithAllowCredentials(element.Interceptors.Cors.AllowCredentials),
rkgrpccors.WithExposeHeaders(element.Interceptors.Cors.ExposeHeaders...),
rkgrpccors.WithMaxAge(element.Interceptors.Cors.MaxAge),
rkgrpccors.WithAllowHeaders(element.Interceptors.Cors.AllowHeaders...),
rkgrpccors.WithAllowMethods(element.Interceptors.Cors.AllowMethods...),
}

entry.AddGwCorsOptions(opts...)
}

// did we enabled meta interceptor?
if element.Interceptors.Meta.Enabled {
opts := []rkgrpcmeta.Option{
Expand Down Expand Up @@ -771,7 +798,7 @@ func WithGrpcDialOptionsGrpc(opts ...grpc.DialOption) GrpcEntryOption {
}
}

// GwMuxOptions Provide gateway server mux options.
// WithGwMuxOptionsGrpc Provide gateway server mux options.
func WithGwMuxOptionsGrpc(opts ...gwruntime.ServeMuxOption) GrpcEntryOption {
return func(entry *GrpcEntry) {
entry.GwMuxOptions = append(entry.GwMuxOptions, opts...)
Expand Down Expand Up @@ -806,6 +833,7 @@ func RegisterGrpcEntry(opts ...GrpcEntryOption) *GrpcEntry {
GwHttpToGrpcMapping: make(map[string]*gwRule),
GwDialOptions: make([]grpc.DialOption, 0),
HttpMux: http.NewServeMux(),
gwCorsOptions: make([]rkgrpccors.Option, 0),
}

for i := range opts {
Expand Down Expand Up @@ -907,6 +935,11 @@ func (entry *GrpcEntry) AddStreamInterceptors(inter ...grpc.StreamServerIntercep
entry.StreamInterceptors = append(entry.StreamInterceptors, inter...)
}

// AddGwCorsOptions Enable CORS at gateway side with options.
func (entry *GrpcEntry) AddGwCorsOptions(opts ...rkgrpccors.Option) {
entry.gwCorsOptions = append(entry.gwCorsOptions, opts...)
}

// AddRegFuncGrpc Add grpc registration func.
func (entry *GrpcEntry) AddRegFuncGrpc(f ...GrpcRegFunc) {
entry.GrpcRegF = append(entry.GrpcRegF, f...)
Expand Down Expand Up @@ -1020,9 +1053,17 @@ func (entry *GrpcEntry) Bootstrap(ctx context.Context) {
entry.HttpMux.Handle(entry.PromEntry.Path, promhttp.HandlerFor(entry.PromEntry.Gatherer, promhttp.HandlerOpts{}))
}
// 5.5: Create http server
var httpHandler http.Handler
httpHandler = entry.HttpMux

// 5.6: If CORS enabled, then add interceptor for grpc-gateway
if len(entry.gwCorsOptions) > 0 {
httpHandler = rkgrpccors.Interceptor(entry.HttpMux, entry.gwCorsOptions...)
}

entry.HttpServer = &http.Server{
Addr: "0.0.0.0:" + strconv.FormatUint(entry.Port, 10),
Handler: h2c.NewHandler(entry.HttpMux, &http2.Server{}),
Handler: h2c.NewHandler(httpHandler, &http2.Server{}),
}

// 6: Bootstrap CommonServiceEntry, SwEntry, PromEntry and TvEntry
Expand Down
2 changes: 2 additions & 0 deletions boot/grpc_entry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ grpc:
reqPerSec: 1
timeout:
enabled: true
cors:
enabled: true
`

// Create bootstrap config file at ut temp dir
Expand Down
13 changes: 13 additions & 0 deletions example/boot/cors/boot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
grpc:
- name: greeter # Required
port: 8080 # Required
enabled: true # Required
commonService:
enabled: true # Optional, default: false
interceptors:
cors:
enabled: true
allowOrigins:
- "http://localhost:8080"
- "http://localhost:*"
24 changes: 24 additions & 0 deletions example/boot/cors/cors.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<body>

<h1>CORS Test</h1>

<p>Call http://localhost:8080/rk/v1/healthy</p>

<script type="text/javascript">
window.onload = function() {
var apiUrl = 'http://localhost:8080/rk/v1/healthy';
fetch(apiUrl).then(response => response.json()).then(data => {
document.getElementById("res").innerHTML = data["healthy"]
}).catch(err => {
document.getElementById("res").innerHTML = err
});
};
</script>

<h4>Response: </h4>
<p id="res"></p>

</body>
</html>
28 changes: 28 additions & 0 deletions example/boot/cors/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) 2021 rookie-ninja
//
// Use of this source code is governed by an Apache-style
// license that can be found in the LICENSE file.
package main

import (
"context"
"github.com/rookie-ninja/rk-entry/entry"
"github.com/rookie-ninja/rk-grpc/boot"
)

func main() {
// Bootstrap basic entries from boot config.
rkentry.RegisterInternalEntriesFromConfig("example/boot/cors/boot.yaml")

// Bootstrap grpc entry from boot config
res := rkgrpc.RegisterGrpcEntriesWithConfig("example/boot/cors/boot.yaml")

// Bootstrap gin entry
res["greeter"].Bootstrap(context.Background())

// Wait for shutdown signal
rkentry.GlobalAppCtx.WaitForShutdownSig()

// Interrupt gin entry
res["greeter"].Interrupt(context.Background())
}
109 changes: 109 additions & 0 deletions interceptor/cors/interceptor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) 2021 rookie-ninja
//
// Use of this source code is governed by an Apache-style
// license that can be found in the LICENSE file.
//
// Package rkgrpccors is a CORS interceptor for grpc framework
package rkgrpccors

import (
"net/http"
"strconv"
"strings"
)

func Interceptor(h http.Handler, opts ...Option) http.Handler {
set := newOptionSet(opts...)

allowMethods := strings.Join(set.AllowMethods, ",")
allowHeaders := strings.Join(set.AllowHeaders, ",")
exposeHeaders := strings.Join(set.ExposeHeaders, ",")
maxAge := strconv.Itoa(set.MaxAge)

return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if set.Skipper(req) {
h.ServeHTTP(w, req)
return
}

originHeader := req.Header.Get(headerOrigin)
preflight := req.Method == http.MethodOptions

// 1: if no origin header was provided, we will return 204 if request is not a OPTION method
if originHeader == "" {
// 1.1: if not a preflight request, then pass through
if !preflight {
h.ServeHTTP(w, req)
return
}

// 1.2: if it is a preflight request, then return with 204
w.WriteHeader(http.StatusNoContent)
return
}

// 2: origin not allowed, we will return 204 if request is not a OPTION method
if !set.isOriginAllowed(originHeader) {
// 2.1: if not a preflight request, then pass through
if !preflight {
w.WriteHeader(http.StatusFound)
return
}

// 2.2: if it is a preflight request, then return with 204
w.WriteHeader(http.StatusNoContent)
return
}

// 3: not a OPTION method
if !preflight {
w.Header().Set(headerAccessControlAllowOrigin, originHeader)
// 3.1: add Access-Control-Allow-Credentials
if set.AllowCredentials {
w.Header().Set(headerAccessControlAllowCredentials, "true")
}
// 3.2: add Access-Control-Expose-Headers
if exposeHeaders != "" {
w.Header().Set(headerAccessControlExposeHeaders, exposeHeaders)
}
h.ServeHTTP(w, req)
return
}

// 4: preflight request, return 204
// add related headers including:
//
// - Vary
// - Access-Control-Allow-Origin
// - Access-Control-Allow-Methods
// - Access-Control-Allow-Credentials
// - Access-Control-Allow-Headers
// - Access-Control-Max-Age
w.Header().Add(headerVary, headerAccessControlRequestMethod)
w.Header().Add(headerVary, headerAccessControlRequestHeaders)
w.Header().Set(headerAccessControlAllowOrigin, originHeader)
w.Header().Set(headerAccessControlAllowMethods, allowMethods)

// 4.1: Access-Control-Allow-Credentials
if set.AllowCredentials {
w.Header().Set(headerAccessControlAllowCredentials, "true")
}

// 4.2: Access-Control-Allow-Headers
if allowHeaders != "" {
w.Header().Set(headerAccessControlAllowHeaders, allowHeaders)
} else {
h := req.Header.Get(headerAccessControlRequestHeaders)
if h != "" {
w.Header().Set(headerAccessControlAllowHeaders, h)
}
}
if set.MaxAge > 0 {
// 4.3: Access-Control-Max-Age
w.Header().Set(headerAccessControlMaxAge, maxAge)
}

w.WriteHeader(http.StatusNoContent)
return
})
}
Loading

0 comments on commit 641955b

Please sign in to comment.