Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add adapter for beego #547

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions pkg/adapters/beego/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
This package provides Sentinel middleware for Beego.

Users may register SentinelMiddleware to the Beego server, like.

import (
sentinelPlugin "github.com/sentinel-group/sentinel-go-adapters/beego"
"github.com/beego/beego/v2/server/web"
)

web.RunWithMiddleWares(":0", middleware)

The plugin extracts "HttpMethod:FullPath" as the resource name by default (e.g. GET:/foo/:id).
Users may provide customized resource name extractor when creating new
SentinelMiddleware (via options).

Fallback logic: the plugin will return "429 Too Many Requests" status code
if current request is blocked by Sentinel rules. Users may also
provide customized fallback logic via WithBlockFallback(handler) options.
*/
package beego
43 changes: 43 additions & 0 deletions pkg/adapters/beego/filter_chain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package beego

import (
sentinel "github.com/alibaba/sentinel-golang/api"
"github.com/alibaba/sentinel-golang/core/base"
"github.com/beego/beego/v2/server/web"
beegoCtx "github.com/beego/beego/v2/server/web/context"
"net/http"
)

// SentinelFilterChain returns new web.FilterChain.
// Default resource name pattern is {httpMethod}:{apiPath}, such as "GET:/api/:id".
// Default block fallback is to return 429 (Too Many Requests) response.
//
// You may customize your own resource extractor and block handler by setting options.
func SentinelFilterChain(opts ...Option) web.FilterChain {
options := evaluateOptions(opts)
return func(next web.FilterFunc) web.FilterFunc {
return func(ctx *beegoCtx.Context) {
resourceName := ctx.Input.Method() + ":" + ctx.Input.URL()
if options.resourceExtract != nil {
resourceName = options.resourceExtract(ctx.Request)
}
entry, blockErr := sentinel.Entry(
resourceName,
sentinel.WithResourceType(base.ResTypeWeb),
sentinel.WithTrafficType(base.Inbound),
)
if blockErr != nil {
if options.blockFallback != nil {
status, msg := options.blockFallback(ctx.Request)
http.Error(ctx.ResponseWriter, msg, status)
} else {
// default error response
http.Error(ctx.ResponseWriter, "Blocked by Sentinel", http.StatusTooManyRequests)
}
return
}
defer entry.Exit()
next(ctx)
}
}
}
106 changes: 106 additions & 0 deletions pkg/adapters/beego/filter_chain_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package beego

import (
"github.com/alibaba/sentinel-golang/core/flow"
"github.com/beego/beego/v2/server/web"
beegoCtx "github.com/beego/beego/v2/server/web/context"
"io"
"net/http"
"net/http/httptest"
"testing"
)

func TestSentinelFilterChain(t *testing.T) {
type args struct {
opts []Option
method string
path string
reqPath string
handlerFunc web.HandleFunc
body io.Reader
}
type want struct {
code int
}
tests := []struct {
name string
args args
want want
}{
{
name: "default get",
args: args{
opts: []Option{},
method: http.MethodGet,
path: "/test",
reqPath: "/test",
handlerFunc: func(ctx *beegoCtx.Context) {
_ = ctx.Resp([]byte("hello"))
},
body: nil,
},
want: want{
code: http.StatusOK,
},
},
{
name: "customize resource extract",
args: args{
opts: []Option{
WithResourceExtractor(func(r *http.Request) string {
return "customize_block_fallback"
}),
},
method: http.MethodPost,
path: "/api/users/:id",
reqPath: "/api/users/123",
handlerFunc: func(ctx *beegoCtx.Context) {
_ = ctx.Resp([]byte("pong"))
},
body: nil,
},
want: want{
code: http.StatusTooManyRequests,
},
},
{
name: "customize block fallback",
args: args{
opts: []Option{
WithBlockFallback(func(r *http.Request) (int, string) {
return http.StatusInternalServerError, "customize block fallback"
}),
},
method: http.MethodGet,
path: "/block",
reqPath: "/block",
handlerFunc: func(ctx *beegoCtx.Context) {
_ = ctx.Resp([]byte("pong"))
},
body: nil,
},
want: want{
code: http.StatusInternalServerError,
},
},
}
initSentinel(t)
defer func() {
_ = flow.ClearRules()
}()

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cr := web.NewControllerRegister()
cr.Get(tt.args.reqPath, tt.args.handlerFunc)

cr.InsertFilterChain("/*", SentinelFilterChain(tt.args.opts...))
cr.Init()

r := httptest.NewRequest(tt.args.method, tt.args.reqPath, tt.args.body)
w := httptest.NewRecorder()

cr.ServeHTTP(w, r)
})
}
}
8 changes: 8 additions & 0 deletions pkg/adapters/beego/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/alibaba/sentinel-golang/pkg/adapters/beego

go 1.13

require (
github.com/alibaba/sentinel-golang v1.0.4
github.com/beego/beego/v2 v2.1.1
)
42 changes: 42 additions & 0 deletions pkg/adapters/beego/middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package beego

import (
sentinel "github.com/alibaba/sentinel-golang/api"
"github.com/alibaba/sentinel-golang/core/base"
"github.com/beego/beego/v2/server/web"
"net/http"
)

// SentinelMiddleware returns new web.MiddleWare.
// Default resource name pattern is {httpMethod}:{apiPath}, such as "GET:/api/:id".
// Default block fallback is to return 429 (Too Many Requests) response.
//
// You may customize your own resource extractor and block handler by setting options.
func SentinelMiddleware(opts ...Option) web.MiddleWare {
options := evaluateOptions(opts)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resourceName := r.Method + ":" + r.URL.Path
if options.resourceExtract != nil {
resourceName = options.resourceExtract(r)
}
entry, blockErr := sentinel.Entry(
resourceName,
sentinel.WithResourceType(base.ResTypeWeb),
sentinel.WithTrafficType(base.Inbound),
)
if blockErr != nil {
if options.blockFallback != nil {
status, msg := options.blockFallback(r)
http.Error(w, msg, status)
} else {
// default error response
http.Error(w, "Blocked by Sentinel", http.StatusTooManyRequests)
}
return
}
defer entry.Exit()
next.ServeHTTP(w, r)
})
}
}
31 changes: 31 additions & 0 deletions pkg/adapters/beego/middleware_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package beego

import (
"github.com/beego/beego/v2/server/web"
beegoCtx "github.com/beego/beego/v2/server/web/context"
"net/http"
)

func Example() {
opts := []Option{
// customize resource extractor if required
// method_path by default
WithResourceExtractor(func(r *http.Request) string {
return r.Header.Get("X-Real-IP")
}),
// customize block fallback if required
// abort with status 429 by default
WithBlockFallback(func(r *http.Request) (int, string) {
return 400, "too many request; the quota used up"
}),
}

web.Get("/test", func(ctx *beegoCtx.Context) {
})

// Routing filter chain
web.InsertFilterChain("/*", SentinelFilterChain(opts...))

// Global middleware
web.RunWithMiddleWares(":0", SentinelMiddleware(opts...))
}
Loading