Create & Init Project...

This commit is contained in:
2019-04-22 18:49:16 +08:00
commit fc4fa37393
25440 changed files with 4054998 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
### business/blademaster
##### Version 1.0.0
1. 添加基础模块与测试:
- Antispam
- Limiter
- Supervisor
- Degrade

View File

@@ -0,0 +1,8 @@
# Author
maojian
lintnaghui
caoguoliang
zhoujiahui
# Reviewer
maojian

View File

@@ -0,0 +1,12 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- caoguoliang
- lintnaghui
- maojian
- zhoujiahui
reviewers:
- caoguoliang
- lintnaghui
- maojian
- zhoujiahui

View File

@@ -0,0 +1,26 @@
#### business/blademaster
> Out of Box Middleware
##### 项目简介
来自 bilibili 主站技术部的 blademaster middleware目前以下 middleware 已经 Ready for Production
- Antispam
- Limiter
- Supervisor
- Degrade
##### 项目特点
- 模块化设计,一个模块只干一件事
##### 编译环境
- **请只用 Golang v1.8.x 以上版本编译执行**
##### 依赖包
###### Limiter:
- [x/time/rate](golang.org/x/time/rate)

View File

@@ -0,0 +1,64 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["antispam_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//library/cache/redis:go_default_library",
"//library/container/pool:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = ["antispam.go"],
importpath = "go-common/library/net/http/blademaster/middleware/antispam",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/cache/redis:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
],
)
go_test(
name = "go_default_xtest",
srcs = ["example_test.go"],
tags = ["automanaged"],
deps = [
"//library/cache/redis:go_default_library",
"//library/container/pool:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/antispam:go_default_library",
"//library/time:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,5 @@
### business/blademaster/antispam
##### Version 1.0.0
1. 完成基本功能与测试

View File

@@ -0,0 +1,6 @@
# Author
lintnaghui
caoguoliang
# Reviewer
maojian

View File

@@ -0,0 +1,9 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- caoguoliang
- lintnaghui
reviewers:
- caoguoliang
- lintnaghui
- maojian

View File

@@ -0,0 +1,13 @@
#### business/blademaster/antispam
##### 项目简介
blademaster 的 antispam middleware主要用于限制用户的请求频率
##### 编译环境
- **请只用 Golang v1.8.x 以上版本编译执行**
##### 依赖包
- No other dependency

View File

@@ -0,0 +1,139 @@
package antispam
import (
"fmt"
"time"
"go-common/library/cache/redis"
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"github.com/pkg/errors"
)
const (
_prefixRate = "r_%d_%s_%d"
_prefixTotal = "t_%d_%s_%d"
// antispam
_defSecond = 1
_defHour = 1
)
// Antispam is a antispam instance.
type Antispam struct {
redis *redis.Pool
conf *Config
}
// Config antispam config.
type Config struct {
On bool // switch on/off
Second int // every N second allow N requests.
N int // one unit allow N requests.
Hour int // every N hour allow M requests.
M int // one winodw allow M requests.
Redis *redis.Config
}
func (c *Config) validate() error {
if c == nil {
return errors.New("antispam: empty config")
}
if c.Second < _defSecond {
return errors.New("antispam: invalid Second")
}
if c.Hour < _defHour {
return errors.New("antispam: invalid Hour")
}
return nil
}
// New new a antispam service.
func New(c *Config) (s *Antispam) {
if err := c.validate(); err != nil {
panic(err)
}
s = &Antispam{
redis: redis.NewPool(c.Redis),
}
s.Reload(c)
return s
}
// Reload reload antispam config.
func (s *Antispam) Reload(c *Config) {
if err := c.validate(); err != nil {
log.Error("Failed to reload antispam: %+v", err)
return
}
s.conf = c
}
// Rate antispam by user + path.
func (s *Antispam) Rate(c *bm.Context, second, count int) (err error) {
mid, ok := c.Get("mid")
if !ok {
return
}
curSecond := int(time.Now().Unix())
burst := curSecond - curSecond%second
key := rateKey(mid.(int64), c.Request.URL.Path, burst)
return s.antispam(c, key, second, count)
}
// Total antispam by user + path.
func (s *Antispam) Total(c *bm.Context, hour, count int) (err error) {
second := hour * 3600
mid, ok := c.Get("mid")
if !ok {
return
}
curHour := int(time.Now().Unix() / 3600)
burst := curHour - curHour%hour
key := totalKey(mid.(int64), c.Request.URL.Path, burst)
return s.antispam(c, key, second, count)
}
func (s *Antispam) antispam(c *bm.Context, key string, interval, count int) error {
conn := s.redis.Get(c)
defer conn.Close()
incred, err := redis.Int64(conn.Do("INCR", key))
if err != nil {
return nil
}
if incred == 1 {
conn.Do("EXPIRE", key, interval)
}
if incred > int64(count) {
return ecode.LimitExceed
}
return nil
}
func rateKey(mid int64, path string, burst int) string {
return fmt.Sprintf(_prefixRate, mid, path, burst)
}
func totalKey(mid int64, path string, burst int) string {
return fmt.Sprintf(_prefixTotal, mid, path, burst)
}
func (s *Antispam) ServeHTTP(ctx *bm.Context) {
if err := s.Rate(ctx, s.conf.Second, s.conf.N); err != nil {
ctx.JSON(nil, ecode.LimitExceed)
ctx.Abort()
return
}
if err := s.Total(ctx, s.conf.Hour, s.conf.M); err != nil {
ctx.JSON(nil, ecode.LimitExceed)
ctx.Abort()
return
}
}
// Handler is antispam handle.
func (s *Antispam) Handler() bm.HandlerFunc {
return s.ServeHTTP
}

View File

@@ -0,0 +1,108 @@
package antispam
import (
"context"
"io/ioutil"
"net/http"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go-common/library/cache/redis"
"go-common/library/container/pool"
bm "go-common/library/net/http/blademaster"
xtime "go-common/library/time"
)
func TestAntiSpamHandler(t *testing.T) {
anti := New(
&Config{
On: true,
Second: 1,
N: 1,
Hour: 1,
M: 1,
Redis: &redis.Config{
Config: &pool.Config{
Active: 10,
Idle: 10,
IdleTimeout: xtime.Duration(time.Second * 60),
},
Name: "test",
Proto: "tcp",
Addr: "172.18.33.60:6889",
DialTimeout: xtime.Duration(time.Second),
ReadTimeout: xtime.Duration(time.Second),
WriteTimeout: xtime.Duration(time.Second),
},
},
)
engine := bm.New()
engine.UseFunc(func(c *bm.Context) {
mid, _ := strconv.ParseInt(c.Request.Form.Get("mid"), 10, 64)
c.Set("mid", mid)
c.Next()
})
engine.Use(anti.Handler())
engine.GET("/antispam", func(c *bm.Context) {
c.String(200, "pass")
})
go engine.Run(":18080")
time.Sleep(time.Millisecond * 50)
code, content, err := httpGet("http://127.0.0.1:18080/antispam?mid=11")
if err != nil {
t.Logf("http get failed, err:=%v", err)
t.FailNow()
}
if code != 200 || string(content) != "pass" {
t.Logf("request should pass by limiter, but blocked: %d, %v", code, content)
t.FailNow()
}
_, content, err = httpGet("http://127.0.0.1:18080/antispam?mid=11")
if err != nil {
t.Logf("http get failed, err:=%v", err)
t.FailNow()
}
if string(content) == "pass" {
t.Logf("request should block by limiter, but passed")
t.FailNow()
}
engine.Server().Shutdown(context.TODO())
}
func httpGet(url string) (code int, content []byte, err error) {
resp, err := http.Get(url)
if err != nil {
return
}
defer resp.Body.Close()
content, err = ioutil.ReadAll(resp.Body)
if err != nil {
return
}
code = resp.StatusCode
return
}
func TestConfigValidate(t *testing.T) {
var conf *Config
assert.Contains(t, conf.validate().Error(), "empty config")
conf = &Config{
Second: 0,
}
assert.Contains(t, conf.validate().Error(), "invalid Second")
conf = &Config{
Second: 1,
Hour: 0,
}
assert.Contains(t, conf.validate().Error(), "invalid Hour")
}

View File

@@ -0,0 +1,45 @@
package antispam_test
import (
"time"
"go-common/library/cache/redis"
"go-common/library/container/pool"
"go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/antispam"
xtime "go-common/library/time"
)
// This example create a antispam middleware instance and attach to a blademaster engine,
// it will protect '/ping' API with specified policy.
// If anyone who requests this API more frequently than 1 req/second or 1 req/hour,
// a StatusServiceUnavailable error will be raised.
func Example() {
anti := antispam.New(&antispam.Config{
On: true,
Second: 1,
N: 1,
Hour: 1,
M: 1,
Redis: &redis.Config{
Config: &pool.Config{
Active: 10,
Idle: 10,
IdleTimeout: xtime.Duration(time.Second * 60),
},
Name: "test",
Proto: "tcp",
Addr: "172.18.33.60:6889",
DialTimeout: xtime.Duration(time.Second),
ReadTimeout: xtime.Duration(time.Second),
WriteTimeout: xtime.Duration(time.Second),
},
})
engine := blademaster.Default()
engine.Use(anti)
engine.GET("/ping", func(c *blademaster.Context) {
c.String(200, "%s", "pong")
})
engine.Run(":18080")
}

View File

@@ -0,0 +1,67 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["auth_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/metadata:go_default_library",
"//library/net/netutil/breaker:go_default_library",
"//library/net/rpc/warden:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = ["auth.go"],
importpath = "go-common/library/net/http/blademaster/middleware/auth",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/main/identify/api/grpc:go_default_library",
"//library/ecode:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/metadata:go_default_library",
"//library/net/rpc/warden:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_xtest",
srcs = ["example_test.go"],
tags = ["automanaged"],
deps = [
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/auth:go_default_library",
"//library/net/metadata:go_default_library",
"//library/net/rpc/warden:go_default_library",
],
)

View File

@@ -0,0 +1,13 @@
#### library/net/http/blademaster/middleware/auth
##### Version 1.0.2
1. 将认证使用的方法作为公开方法
##### Version 1.0.1
1. 成功后将 mid 加入 metadata
##### Version 1.0.0
1. 完全使用 identify-service 来认证用户

View File

@@ -0,0 +1,12 @@
# Owner
maojian
zhoujiahui
# Author
maojian
zhoujiahui
# Reviewer
maojian
haoguanwei
wanghuan01

View File

@@ -0,0 +1,10 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- maojian
- zhoujiahui
reviewers:
- haoguanwei
- maojian
- wanghuan01
- zhoujiahui

View File

@@ -0,0 +1,13 @@
#### library/net/http/blademaster/middleware/auth
##### 项目简介
blademaster 的 authorization middleware主要用于设置路由的认证策略
##### 编译环境
- **请只用 Golang v1.10.x 以上版本编译执行**
##### 依赖包
- No other dependency

View File

@@ -0,0 +1,177 @@
package auth
import (
idtv1 "go-common/app/service/main/identify/api/grpc"
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/metadata"
"go-common/library/net/rpc/warden"
"github.com/pkg/errors"
)
// Config is the identify config model.
type Config struct {
Identify *warden.ClientConfig
// csrf switch.
DisableCSRF bool
}
// Auth is the authorization middleware
type Auth struct {
idtv1.IdentifyClient
conf *Config
}
// authFunc will return mid and error by given context
type authFunc func(*bm.Context) (int64, error)
var _defaultConf = &Config{
Identify: nil,
DisableCSRF: false,
}
// New is used to create an authorization middleware
func New(conf *Config) *Auth {
if conf == nil {
conf = _defaultConf
}
identify, err := idtv1.NewClient(conf.Identify)
if err != nil {
panic(errors.WithMessage(err, "Failed to dial identify service"))
}
auth := &Auth{
IdentifyClient: identify,
conf: conf,
}
return auth
}
// User is used to mark path as access required.
// If `access_key` is exist in request form, it will using mobile access policy.
// Otherwise to web access policy.
func (a *Auth) User(ctx *bm.Context) {
req := ctx.Request
if req.Form.Get("access_key") == "" {
a.UserWeb(ctx)
return
}
a.UserMobile(ctx)
}
// UserWeb is used to mark path as web access required.
func (a *Auth) UserWeb(ctx *bm.Context) {
a.midAuth(ctx, a.AuthCookie)
}
// UserMobile is used to mark path as mobile access required.
func (a *Auth) UserMobile(ctx *bm.Context) {
a.midAuth(ctx, a.AuthToken)
}
// Guest is used to mark path as guest policy.
// If `access_key` is exist in request form, it will using mobile access policy.
// Otherwise to web access policy.
func (a *Auth) Guest(ctx *bm.Context) {
req := ctx.Request
if req.Form.Get("access_key") == "" {
a.GuestWeb(ctx)
return
}
a.GuestMobile(ctx)
}
// GuestWeb is used to mark path as web guest policy.
func (a *Auth) GuestWeb(ctx *bm.Context) {
a.guestAuth(ctx, a.AuthCookie)
}
// GuestMobile is used to mark path as mobile guest policy.
func (a *Auth) GuestMobile(ctx *bm.Context) {
a.guestAuth(ctx, a.AuthToken)
}
// AuthToken is used to authorize request by token
func (a *Auth) AuthToken(ctx *bm.Context) (int64, error) {
req := ctx.Request
key := req.Form.Get("access_key")
if key == "" {
return 0, ecode.NoLogin
}
buvid := req.Header.Get("buvid")
reply, err := a.GetTokenInfo(ctx, &idtv1.GetTokenInfoReq{Token: key, Buvid: buvid})
if err != nil {
return 0, err
}
if !reply.IsLogin {
return 0, ecode.NoLogin
}
return reply.Mid, nil
}
// AuthCookie is used to authorize request by cookie
func (a *Auth) AuthCookie(ctx *bm.Context) (int64, error) {
req := ctx.Request
ssDaCk, _ := req.Cookie("SESSDATA")
if ssDaCk == nil {
return 0, ecode.NoLogin
}
cookie := req.Header.Get("Cookie")
reply, err := a.GetCookieInfo(ctx, &idtv1.GetCookieInfoReq{Cookie: cookie})
if err != nil {
return 0, err
}
if !reply.IsLogin {
return 0, ecode.NoLogin
}
// check csrf
clientCsrf := req.FormValue("csrf")
if a.conf != nil && !a.conf.DisableCSRF && req.Method == "POST" {
if clientCsrf != reply.Csrf {
return 0, ecode.CsrfNotMatchErr
}
}
return reply.Mid, nil
}
func (a *Auth) midAuth(ctx *bm.Context, auth authFunc) {
mid, err := auth(ctx)
if err != nil {
ctx.JSON(nil, err)
ctx.Abort()
return
}
setMid(ctx, mid)
}
func (a *Auth) guestAuth(ctx *bm.Context, auth authFunc) {
mid, err := auth(ctx)
// no error happened and mid is valid
if err == nil && mid > 0 {
setMid(ctx, mid)
return
}
ec := ecode.Cause(err)
if ec.Equal(ecode.CsrfNotMatchErr) {
ctx.JSON(nil, ec)
ctx.Abort()
return
}
}
// set mid into context
// NOTE: This method is not thread safe.
func setMid(ctx *bm.Context, mid int64) {
ctx.Set("mid", mid)
if md, ok := metadata.FromContext(ctx); ok {
md[metadata.Mid] = mid
return
}
}

View File

@@ -0,0 +1,341 @@
package auth
import (
"bytes"
"context"
"fmt"
"mime/multipart"
"net/http"
"net/url"
"testing"
"time"
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/metadata"
"go-common/library/net/netutil/breaker"
"go-common/library/net/rpc/warden"
xtime "go-common/library/time"
"github.com/stretchr/testify/assert"
)
const (
_testUID = "2231365"
)
type Response struct {
Code int `json:"code"`
Data string `json:"data"`
}
func init() {
log.Init(&log.Config{
Stdout: true,
})
}
func client() *bm.Client {
return bm.NewClient(&bm.ClientConfig{
App: &bm.App{
Key: "53e2fa226f5ad348",
Secret: "3cf6bd1b0ff671021da5f424fea4b04a",
},
Dial: xtime.Duration(time.Second),
Timeout: xtime.Duration(time.Second),
KeepAlive: xtime.Duration(time.Second * 10),
Breaker: &breaker.Config{
Window: xtime.Duration(time.Second),
Sleep: xtime.Duration(time.Millisecond * 100),
Bucket: 10,
Ratio: 0.5,
Request: 100,
},
})
}
func create() *Auth {
return New(&Config{
Identify: &warden.ClientConfig{},
DisableCSRF: false,
})
}
func engine() *bm.Engine {
e := bm.NewServer(nil)
authn := create()
e.GET("/user", authn.User, func(ctx *bm.Context) {
mid, _ := ctx.Get("mid")
ctx.JSON(fmt.Sprintf("%d", mid), nil)
})
e.GET("/metadata/user", authn.User, func(ctx *bm.Context) {
mid := metadata.Value(ctx, metadata.Mid)
ctx.JSON(fmt.Sprintf("%d", mid.(int64)), nil)
})
e.GET("/mobile", authn.UserMobile, func(ctx *bm.Context) {
mid, _ := ctx.Get("mid")
ctx.JSON(fmt.Sprintf("%d", mid), nil)
})
e.GET("/metadata/mobile", authn.UserMobile, func(ctx *bm.Context) {
mid := metadata.Value(ctx, metadata.Mid)
ctx.JSON(fmt.Sprintf("%d", mid.(int64)), nil)
})
e.GET("/web", authn.UserWeb, func(ctx *bm.Context) {
mid, _ := ctx.Get("mid")
ctx.JSON(fmt.Sprintf("%d", mid), nil)
})
e.GET("/guest", authn.Guest, func(ctx *bm.Context) {
var (
mid int64
)
if _mid, ok := ctx.Get("mid"); ok {
mid, _ = _mid.(int64)
}
ctx.JSON(fmt.Sprintf("%d", mid), nil)
})
e.GET("/guest/web", authn.GuestWeb, func(ctx *bm.Context) {
var (
mid int64
)
if _mid, ok := ctx.Get("mid"); ok {
mid, _ = _mid.(int64)
}
ctx.JSON(fmt.Sprintf("%d", mid), nil)
})
e.GET("/guest/mobile", authn.GuestMobile, func(ctx *bm.Context) {
var (
mid int64
)
if _mid, ok := ctx.Get("mid"); ok {
mid, _ = _mid.(int64)
}
ctx.JSON(fmt.Sprintf("%d", mid), nil)
})
e.POST("/guest/csrf", authn.Guest, func(ctx *bm.Context) {
var (
mid int64
)
if _mid, ok := ctx.Get("mid"); ok {
mid, _ = _mid.(int64)
}
ctx.JSON(fmt.Sprintf("%d", mid), nil)
})
return e
}
func TestFromNilConfig(t *testing.T) {
New(nil)
}
func TestIdentifyHandler(t *testing.T) {
e := engine()
go e.Run(":18080")
time.Sleep(time.Second)
// test cases
testWebUser(t, "/user")
testWebUser(t, "/metadata/user")
testWebUser(t, "/web")
testWebUser(t, "/guest")
testWebUser(t, "/guest/web")
testWebUserFailed(t, "/user")
testWebUserFailed(t, "/web")
testMobileUser(t, "/user")
testMobileUser(t, "/mobile")
testMobileUser(t, "/metadata/mobile")
testMobileUser(t, "/guest")
testMobileUser(t, "/guest/mobile")
testMobileUserFailed(t, "/user")
testMobileUserFailed(t, "/mobile")
testGuest(t, "/guest")
testGuestCSRF(t, "/guest/csrf")
testGuestCSRFFailed(t, "/guest/csrf")
testMultipartCSRF(t, "/guest/csrf")
if err := e.Server().Shutdown(context.TODO()); err != nil {
t.Logf("Failed to shutdown bm engine: %v", err)
}
}
func testWebUser(t *testing.T, path string) {
res := Response{}
query := url.Values{}
cli := client()
req, err := cli.NewRequest(http.MethodGet, "http://127.0.0.1:18080/"+path, "", query)
assert.NoError(t, err)
req.AddCookie(&http.Cookie{
Name: "DedeUserID",
Value: _testUID,
})
req.AddCookie(&http.Cookie{
Name: "DedeUserID__ckMd5",
Value: "36976f7a5cb6e4a6",
})
req.AddCookie(&http.Cookie{
Name: "SESSDATA",
Value: "7bf20cf0%2C1540627371%2C8ec39f0c",
})
err = cli.Do(context.TODO(), req, &res)
assert.NoError(t, err)
assert.Equal(t, 0, res.Code)
assert.Equal(t, _testUID, res.Data)
}
func testMobileUser(t *testing.T, path string) {
res := Response{}
query := url.Values{}
query.Set("access_key", "cdbd166be6673a5a4f6fbcdd88569edf")
cli := client()
req, err := cli.NewRequest(http.MethodGet, "http://127.0.0.1:18080"+path, "", query)
assert.NoError(t, err)
err = cli.Do(context.TODO(), req, &res)
assert.NoError(t, err)
assert.Equal(t, 0, res.Code)
assert.Equal(t, _testUID, res.Data)
}
func testWebUserFailed(t *testing.T, path string) {
res := Response{}
query := url.Values{}
cli := client()
req, err := cli.NewRequest(http.MethodGet, "http://127.0.0.1:18080/"+path, "", query)
assert.NoError(t, err)
req.AddCookie(&http.Cookie{
Name: "DedeUserID",
Value: _testUID,
})
req.AddCookie(&http.Cookie{
Name: "DedeUserID__ckMd5",
Value: "53c4b106fb4462f1",
})
req.AddCookie(&http.Cookie{
Name: "SESSDATA",
Value: "6eeda532%2C1515837495%2C5a6baa4e",
})
err = cli.Do(context.TODO(), req, &res)
assert.NoError(t, err)
assert.Equal(t, ecode.NoLogin.Code(), res.Code)
assert.Empty(t, res.Data)
}
func testMobileUserFailed(t *testing.T, path string) {
res := Response{}
query := url.Values{}
query.Set("access_key", "5dce488c2ff8d62d7b131da40ae18729")
cli := client()
req, err := cli.NewRequest(http.MethodGet, "http://127.0.0.1:18080"+path, "", query)
assert.NoError(t, err)
err = cli.Do(context.TODO(), req, &res)
assert.NoError(t, err)
assert.Equal(t, ecode.NoLogin.Code(), res.Code)
assert.Empty(t, res.Data)
}
func testGuest(t *testing.T, path string) {
res := Response{}
cli := client()
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:18080"+path, nil)
assert.NoError(t, err)
err = cli.Do(context.TODO(), req, &res)
assert.NoError(t, err)
assert.Equal(t, 0, res.Code)
assert.Equal(t, "0", res.Data)
}
func testGuestCSRF(t *testing.T, path string) {
res := Response{}
param := url.Values{}
param.Set("csrf", "c1524bbf3aa5a1996ff7b1f29a09e796")
cli := client()
req, err := cli.NewRequest(http.MethodPost, "http://127.0.0.1:18080"+path, "", param)
assert.NoError(t, err)
req.AddCookie(&http.Cookie{
Name: "DedeUserID",
Value: _testUID,
})
req.AddCookie(&http.Cookie{
Name: "DedeUserID__ckMd5",
Value: "36976f7a5cb6e4a6",
})
req.AddCookie(&http.Cookie{
Name: "SESSDATA",
Value: "7bf20cf0%2C1540627371%2C8ec39f0c",
})
err = cli.Do(context.TODO(), req, &res)
assert.NoError(t, err)
assert.Equal(t, 0, res.Code)
assert.Equal(t, _testUID, res.Data)
}
func testGuestCSRFFailed(t *testing.T, path string) {
res := Response{}
param := url.Values{}
param.Set("csrf", "invalid-csrf-token")
cli := client()
req, err := cli.NewRequest(http.MethodPost, "http://127.0.0.1:18080"+path, "", param)
assert.NoError(t, err)
req.AddCookie(&http.Cookie{
Name: "DedeUserID",
Value: _testUID,
})
req.AddCookie(&http.Cookie{
Name: "DedeUserID__ckMd5",
Value: "36976f7a5cb6e4a6",
})
req.AddCookie(&http.Cookie{
Name: "SESSDATA",
Value: "7bf20cf0%2C1540627371%2C8ec39f0c",
})
err = cli.Do(context.TODO(), req, &res)
assert.NoError(t, err)
assert.Equal(t, ecode.CsrfNotMatchErr.Code(), res.Code)
assert.Empty(t, res.Data)
}
func testMultipartCSRF(t *testing.T, path string) {
res := Response{}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
writer.WriteField("csrf", "c1524bbf3aa5a1996ff7b1f29a09e796")
writer.Close()
req, err := http.NewRequest("POST", "http://127.0.0.1:18080"+path, body)
assert.NoError(t, err)
req.Header.Set("Content-Type", writer.FormDataContentType())
cli := client()
req.AddCookie(&http.Cookie{
Name: "DedeUserID",
Value: _testUID,
})
req.AddCookie(&http.Cookie{
Name: "DedeUserID__ckMd5",
Value: "36976f7a5cb6e4a6",
})
req.AddCookie(&http.Cookie{
Name: "SESSDATA",
Value: "7bf20cf0%2C1540627371%2C8ec39f0c",
})
err = cli.Do(context.TODO(), req, &res)
assert.NoError(t, err)
assert.Equal(t, 0, res.Code)
assert.Equal(t, _testUID, res.Data)
}

View File

@@ -0,0 +1,45 @@
package auth_test
import (
"fmt"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/auth"
"go-common/library/net/metadata"
"go-common/library/net/rpc/warden"
)
// This example create a identify middleware instance and attach to several path,
// it will validate request by specified policy and put extra information into context. e.g., `mid`.
// It provides additional handler functions to provide the identification for your business handler.
func Example() {
authn := auth.New(&auth.Config{
Identify: &warden.ClientConfig{},
DisableCSRF: false,
})
e := bm.DefaultServer(nil)
// mark `/user` path as User policy
e.GET("/user", authn.User, func(ctx *bm.Context) {
mid := metadata.Int64(ctx, metadata.Mid)
ctx.JSON(fmt.Sprintf("%d", mid), nil)
})
// mark `/mobile` path as UserMobile policy
e.GET("/mobile", authn.UserMobile, func(ctx *bm.Context) {
mid := metadata.Int64(ctx, metadata.Mid)
ctx.JSON(fmt.Sprintf("%d", mid), nil)
})
// mark `/web` path as UserWeb policy
e.GET("/web", authn.UserWeb, func(ctx *bm.Context) {
mid := metadata.Int64(ctx, metadata.Mid)
ctx.JSON(fmt.Sprintf("%d", mid), nil)
})
// mark `/guest` path as Guest policy
e.GET("/guest", authn.Guest, func(ctx *bm.Context) {
mid := metadata.Int64(ctx, metadata.Mid)
ctx.JSON(fmt.Sprintf("%d", mid), nil)
})
e.Run(":18080")
}

View File

@@ -0,0 +1,101 @@
load(
"@io_bazel_rules_go//proto:def.bzl",
"go_proto_library",
)
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
proto_library(
name = "cache_proto",
srcs = ["page.proto"],
tags = ["automanaged"],
deps = ["@gogo_special_proto//github.com/gogo/protobuf/gogoproto"],
)
go_proto_library(
name = "cache_go_proto",
compilers = ["@io_bazel_rules_go//proto:gogofast_proto"],
importpath = "go-common/library/net/http/blademaster/middleware/cache",
proto = ":cache_proto",
tags = ["automanaged"],
deps = ["@com_github_gogo_protobuf//gogoproto:go_default_library"],
)
go_test(
name = "go_default_test",
srcs = ["cache_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//library/cache/memcache:go_default_library",
"//library/container/pool:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/cache/store:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"cache.go",
"control.go",
"degrade.go",
"page.go",
],
embed = [":cache_go_proto"],
importpath = "go-common/library/net/http/blademaster/middleware/cache",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/cache/store:go_default_library",
"@com_github_gogo_protobuf//gogoproto:go_default_library",
"@com_github_gogo_protobuf//proto:go_default_library",
],
)
go_test(
name = "go_default_xtest",
srcs = ["example_test.go"],
tags = ["automanaged"],
deps = [
"//library/cache/memcache:go_default_library",
"//library/container/pool:go_default_library",
"//library/ecode:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/cache:go_default_library",
"//library/net/http/blademaster/middleware/cache/store:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//library/net/http/blademaster/middleware/cache/store:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,10 @@
### business/blademaster/cache
##### Version 1.0.1
1. 添加 Control 策略(目前仅通过 Expires 和 Cache-Control 实现客户端缓存)
##### Version 1.0.0
1. 完成基本功能与测试
2. 完成 Degrade 与 PageCache 逻辑

View File

@@ -0,0 +1,5 @@
# Author
zhoujiahui
# Reviewer
maojian

View File

@@ -0,0 +1,7 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- zhoujiahui
reviewers:
- maojian
- zhoujiahui

View File

@@ -0,0 +1,13 @@
#### business/blademaster/cache
##### 项目简介
blademaster 的通用 cache 模块,一般直接用于缓存返回的 response
##### 编译环境
- **请只用 Golang v1.8.x 以上版本编译执行**
##### 依赖包
- No other dependency

View File

@@ -0,0 +1,38 @@
package cache
import (
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/cache/store"
)
// Cache is the abstract struct for any cache impl
type Cache struct {
store store.Store
}
// Filter is used to check is cache required for every request
type Filter func(*bm.Context) bool
// Policy is used to abstract different cache policy
type Policy interface {
Key(*bm.Context) string
Handler(store.Store) bm.HandlerFunc
}
// New will create a new Cache struct
func New(store store.Store) *Cache {
c := &Cache{
store: store,
}
return c
}
// Cache is used to mark path as customized cache policy
func (c *Cache) Cache(policy Policy, filter Filter) bm.HandlerFunc {
return func(ctx *bm.Context) {
if filter != nil && !filter(ctx) {
return
}
policy.Handler(c.store)(ctx)
}
}

View File

@@ -0,0 +1,353 @@
package cache
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"go-common/library/cache/memcache"
"go-common/library/container/pool"
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/cache/store"
xtime "go-common/library/time"
"github.com/stretchr/testify/assert"
)
const (
SockAddr = "127.0.0.1:18080"
McSockAddr = "172.16.33.54:11211"
)
func uri(base, path string) string {
return fmt.Sprintf("%s://%s%s", "http", base, path)
}
func init() {
log.Init(nil)
}
func newMemcache() (*Cache, func()) {
s := store.NewMemcache(&memcache.Config{
Config: &pool.Config{
Active: 10,
Idle: 2,
IdleTimeout: xtime.Duration(time.Second),
},
Name: "test",
Proto: "tcp",
Addr: McSockAddr,
DialTimeout: xtime.Duration(time.Second),
ReadTimeout: xtime.Duration(time.Second),
WriteTimeout: xtime.Duration(time.Second),
})
return New(s), func() {}
}
func newFile() (*Cache, func()) {
path, err := ioutil.TempDir("", "cache-test")
if err != nil {
panic("Failed to create cache directory")
}
s := store.NewFile(&store.FileConfig{
RootDir: path,
})
remove := func() {
os.RemoveAll(path)
}
return New(s), remove
}
func TestPage(t *testing.T) {
memcache, remove1 := newMemcache()
filestore, remove2 := newFile()
defer func() {
remove1()
remove2()
}()
t.Run("Memcache Store", pageCase(memcache, true))
t.Run("File Store", pageCase(filestore, false))
}
func TestControl(t *testing.T) {
memcache, remove1 := newMemcache()
filestore, remove2 := newFile()
defer func() {
remove1()
remove2()
}()
t.Run("Memcache Store", controlCase(memcache, true))
t.Run("File Store", controlCase(filestore, false))
}
func TestPageCacheMultiWrite(t *testing.T) {
memcache, remove1 := newMemcache()
filestore, remove2 := newFile()
defer func() {
remove1()
remove2()
}()
t.Run("Memcache Store", pageMultiWriteCase(memcache))
t.Run("File Store", pageMultiWriteCase(filestore))
}
func TestDegrade(t *testing.T) {
memcache, remove1 := newMemcache()
filestore, remove2 := newFile()
defer func() {
remove1()
remove2()
}()
t.Run("Memcache Store", degradeCase(memcache))
t.Run("File Store", degradeCase(filestore))
}
func pageCase(cache *Cache, testExpire bool) func(t *testing.T) {
return func(t *testing.T) {
expire := int32(3)
pc := NewPage(expire)
engine := bm.Default()
engine.GET("/page-cache", cache.Cache(pc, nil), func(ctx *bm.Context) {
ctx.Writer.Header().Set("X-Hello", "World")
ctx.String(203, "%s\n", time.Now().String())
})
go engine.Run(SockAddr)
defer func() {
engine.Server().Shutdown(context.Background())
}()
time.Sleep(time.Second)
code1, content1, headers1, err1 := httpGet(uri(SockAddr, "/page-cache"))
code2, content2, headers2, err2 := httpGet(uri(SockAddr, "/page-cache"))
assert.Nil(t, err1)
assert.Nil(t, err2)
assert.Equal(t, code1, 203)
assert.Equal(t, code2, 203)
assert.NotNil(t, content1)
assert.NotNil(t, content2)
assert.Equal(t, headers1["X-Hello"], []string{"World"})
assert.Equal(t, headers2["X-Hello"], []string{"World"})
assert.Equal(t, string(content1), string(content2))
if !testExpire {
return
}
// test if the last caching is expired
t.Logf("Waiting %d seconds for caching expire test", expire+1)
time.Sleep(time.Second * time.Duration(expire+1))
_, content3, _, err3 := httpGet(uri(SockAddr, "/page-cache"))
_, content4, _, err4 := httpGet(uri(SockAddr, "/page-cache"))
assert.Nil(t, err3)
assert.Nil(t, err4)
assert.NotNil(t, content1)
assert.NotNil(t, content2)
assert.NotEqual(t, string(content1), string(content3))
assert.Equal(t, string(content3), string(content4))
}
}
func pageMultiWriteCase(cache *Cache) func(t *testing.T) {
return func(t *testing.T) {
chunks := []string{
"Hello",
"World",
"Hello",
"World",
"Hello",
"World",
"Hello",
"World",
}
pc := NewPage(3)
engine := bm.Default()
engine.GET("/page-cache-write", cache.Cache(pc, nil), func(ctx *bm.Context) {
ctx.Writer.Header().Set("X-Hello", "World")
ctx.Writer.WriteHeader(203)
for _, chunk := range chunks {
ctx.Writer.Write([]byte(chunk))
}
})
go engine.Run(SockAddr)
defer func() {
engine.Server().Shutdown(context.Background())
}()
time.Sleep(time.Second)
code1, content1, headers1, err1 := httpGet(uri(SockAddr, "/page-cache-write"))
code2, content2, headers2, err2 := httpGet(uri(SockAddr, "/page-cache-write"))
assert.Nil(t, err1)
assert.Nil(t, err2)
assert.Equal(t, code1, 203)
assert.Equal(t, code2, 203)
assert.NotNil(t, content1)
assert.NotNil(t, content2)
assert.Equal(t, headers1["X-Hello"], []string{"World"})
assert.Equal(t, headers2["X-Hello"], []string{"World"})
assert.Equal(t, strings.Join(chunks, ""), string(content1))
assert.Equal(t, strings.Join(chunks, ""), string(content2))
assert.Equal(t, string(content1), string(content2))
}
}
func degradeCase(cache *Cache) func(t *testing.T) {
return func(t *testing.T) {
wg := sync.WaitGroup{}
i := int32(0)
degrade := NewDegrader(10)
engine := bm.Default()
engine.GET("/scheduled/error", cache.Cache(degrade.Args("name", "age"), nil), func(c *bm.Context) {
code := atomic.AddInt32(&i, 1)
if code == 5 {
c.JSON("succeed", nil)
return
}
if code%2 == 0 {
c.JSON("", ecode.Degrade)
return
}
c.JSON(fmt.Sprintf("Code: %d", code), ecode.Int(int(code)))
})
wg.Add(1)
go func() {
engine.Run(":18080")
wg.Done()
}()
defer func() {
engine.Server().Shutdown(context.TODO())
wg.Wait()
}()
time.Sleep(time.Second)
for index := 1; index < 10; index++ {
_, content, _, _ := httpGet(uri(SockAddr, "/scheduled/error?name=degrader&age=26"))
t.Log(index, string(content))
var res struct {
Data string `json:"data"`
}
err := json.Unmarshal(content, &res)
assert.Nil(t, err)
if index == 5 {
// ensure response is write to cache
time.Sleep(time.Second)
}
if index > 5 && index%2 == 0 {
if res.Data != "succeed" {
t.Fatalf("Failed to degrade at index: %d", index)
} else {
t.Logf("This request is degraded at index: %d", index)
}
}
}
}
}
func controlCase(cache *Cache, testExpire bool) func(t *testing.T) {
return func(t *testing.T) {
wg := sync.WaitGroup{}
i := int32(0)
expire := int32(30)
control := NewControl(expire)
filter := func(ctx *bm.Context) bool {
if ctx.Request.Form.Get("cache") == "false" {
return false
}
return true
}
engine := bm.Default()
engine.GET("/large/response", cache.Cache(control, filter), func(c *bm.Context) {
c.JSON(map[string]interface{}{
"index": atomic.AddInt32(&i, 1),
"Hello0": "World",
"Hello1": "World",
"Hello2": "World",
"Hello3": "World",
"Hello4": "World",
"Hello5": "World",
"Hello6": "World",
"Hello7": "World",
"Hello8": "World",
}, nil)
})
engine.GET("/large/response/error", cache.Cache(control, filter), func(c *bm.Context) {
c.JSON(nil, ecode.RequestErr)
})
wg.Add(1)
go func() {
engine.Run(":18080")
wg.Done()
}()
defer func() {
engine.Server().Shutdown(context.TODO())
wg.Wait()
}()
time.Sleep(time.Second)
code, content, headers, err := httpGet(uri(SockAddr, "/large/response?name=hello&age=1"))
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.NotEmpty(t, content)
assert.Equal(t, "max-age=30", headers.Get("Cache-Control"))
exp, err := http.ParseTime(headers.Get("Expires"))
assert.NoError(t, err)
assert.InDelta(t, 30, exp.Unix()-time.Now().Unix(), 5)
code, content, headers, err = httpGet(uri(SockAddr, "/large/response/error?name=hello&age=1&cache=false"))
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.NotEmpty(t, content)
assert.Empty(t, headers.Get("Expires"))
assert.Empty(t, headers.Get("Cache-Control"))
code, content, headers, err = httpGet(uri(SockAddr, "/large/response/error?name=hello&age=1"))
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.NotEmpty(t, content)
assert.Empty(t, headers.Get("Expires"))
assert.Empty(t, headers.Get("Cache-Control"))
}
}
func httpGet(url string) (code int, content []byte, headers http.Header, err error) {
resp, err := http.Get(url)
if err != nil {
return
}
defer resp.Body.Close()
if content, err = ioutil.ReadAll(resp.Body); err != nil {
return
}
code = resp.StatusCode
headers = resp.Header
return
}

View File

@@ -0,0 +1,74 @@
package cache
import (
fmt "fmt"
"net/http"
"sync"
"time"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/cache/store"
)
const (
_maxMaxAge = 60 * 5 // 5 minutes
)
// Control is used to work as client side cache orchestrator
type Control struct {
MaxAge int32
pool sync.Pool
}
type controlWriter struct {
*Control
ctx *bm.Context
response http.ResponseWriter
}
var _ http.ResponseWriter = &controlWriter{}
// NewControl will create a new control cache struct
func NewControl(maxAge int32) *Control {
if maxAge > _maxMaxAge {
panic("MaxAge should be less than 300 seconds")
}
ctl := &Control{
MaxAge: maxAge,
}
ctl.pool.New = func() interface{} {
return &controlWriter{}
}
return ctl
}
// Key method is not needed in this situation
func (ctl *Control) Key(ctx *bm.Context) string { return "" }
// Handler is used to execute cache service
func (ctl *Control) Handler(_ store.Store) bm.HandlerFunc {
return func(ctx *bm.Context) {
writer := ctl.pool.Get().(*controlWriter)
writer.Control = ctl
writer.ctx = ctx
writer.response = ctx.Writer
ctx.Writer = writer
ctx.Next()
ctl.pool.Put(writer)
}
}
func (w *controlWriter) Header() http.Header { return w.response.Header() }
func (w *controlWriter) Write(data []byte) (size int, err error) { return w.response.Write(data) }
func (w *controlWriter) WriteHeader(code int) {
// do not inject header if this is an error response
if w.ctx.Error == nil {
headers := w.Header()
headers.Set("Expires", time.Now().UTC().Add(time.Duration(w.MaxAge)*time.Second).Format(http.TimeFormat))
headers.Set("Cache-Control", fmt.Sprintf("max-age=%d", w.MaxAge))
}
w.response.WriteHeader(code)
}

View File

@@ -0,0 +1,219 @@
package cache
import (
"context"
"crypto/md5"
"fmt"
"net/http"
"strings"
"sync"
"sync/atomic"
"time"
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/cache/store"
)
const (
_degradeInterval = 60 * 10
_degradePrefix = "bm.degrade"
)
var (
_degradeBytes = []byte(fmt.Sprintf("{\"code\":%d, \"message\":\"\"}", ecode.Degrade))
)
// Degrader is the common degrader instance.
type Degrader struct {
lock sync.RWMutex
urls map[string]*state
expire int32
ch chan *result
pool sync.Pool // degradeWriter pool
}
// argsDegrader means the degrade will happened by args policy
type argsDegrader struct {
*Degrader
args []string
}
type degradeWriter struct {
*Degrader
ctx *bm.Context
response http.ResponseWriter
store store.Store
key string
state *state
}
type state struct {
// FIXME(zhoujiahui): using transient map to avoid potential memory leak?
// record last cached time
sync.RWMutex
gens map[string]*int64
}
type result struct {
key string
value []byte
store store.Store
}
var _ http.ResponseWriter = &degradeWriter{}
var _ Policy = &argsDegrader{}
// NewDegrader will create a new degrade struct
func NewDegrader(expire int32) (d *Degrader) {
d = &Degrader{
urls: make(map[string]*state),
ch: make(chan *result, 1024),
expire: expire,
}
d.pool.New = func() interface{} {
return &degradeWriter{
Degrader: d,
}
}
go d.degradeproc()
return
}
func (d *Degrader) degradeproc() {
for {
r := <-d.ch
if err := r.store.Set(context.Background(), r.key, r.value, d.expire); err != nil {
log.Error("store write key(%s) error(%v)", r.key, err)
}
}
}
// Args means this path will be degrade by specified args
func (d *Degrader) Args(args ...string) Policy {
return &argsDegrader{
Degrader: d,
args: args,
}
}
func (d *Degrader) state(path string) *state {
d.lock.RLock()
s, ok := d.urls[path]
d.lock.RUnlock()
if !ok {
s = &state{
gens: make(map[string]*int64),
}
d.lock.Lock()
d.urls[path] = s
d.lock.Unlock()
}
return s
}
// Key is used to identify response cache key in most key-value store
func (ad *argsDegrader) Key(ctx *bm.Context) string {
req := ctx.Request
path := req.URL.Path
params := req.Form
vs := make([]string, 0, len(ad.args))
for _, arg := range ad.args {
vs = append(vs, params.Get(arg))
}
return fmt.Sprintf("%s:%s_%x", _degradePrefix, strings.Replace(path, "/", "_", -1), md5.Sum([]byte(strings.Join(vs, "-"))))
}
// Handler is used to execute degrade service
func (ad *argsDegrader) Handler(store store.Store) bm.HandlerFunc {
return func(ctx *bm.Context) {
req := ctx.Request
path := req.URL.Path
writer := ad.pool.Get().(*degradeWriter)
writer.response = ctx.Writer
writer.ctx = ctx
writer.store = store
writer.state = ad.state(path)
writer.key = ad.Key(ctx)
ctx.Writer = writer // replace to degrade writer
ctx.Next()
ad.pool.Put(writer)
}
}
func (w *degradeWriter) Header() http.Header { return w.response.Header() }
func (w *degradeWriter) WriteHeader(code int) { w.response.WriteHeader(code) }
func (w *degradeWriter) Write(data []byte) (size int, err error) {
e := w.ctx.Error
// if an degrade error code is raised from upstream,
// degrade this request directly
if e != nil {
if ec := ecode.Cause(e); ec.Code() == ecode.Degrade.Code() {
return w.write()
}
}
// write origin response
if size, err = w.response.Write(data); err != nil {
return
}
// error raised, this is a unsuccessful response
if e != nil {
return
}
// is required to cache
if !w.state.required(w.key) {
return
}
// async cache succeeded response for further degradation
select {
case w.ch <- &result{key: w.key, value: data, store: w.store}:
default:
}
return
}
func (w *degradeWriter) write() (int, error) {
data, err := w.store.Get(w.ctx, w.key)
if err != nil || len(data) == 0 {
// FIXME(zhoujiahui): The default response data should be respect to render type or content-type header
data = _degradeBytes
}
return w.response.Write(data)
}
// check is required to cache response
// it depends on last cache time and _degradeInterval
func (st *state) required(key string) bool {
now := time.Now().Unix()
st.RLock()
pLast, ok := st.gens[key]
st.RUnlock()
if !ok {
st.Lock()
pLast = new(int64)
st.gens[key] = pLast
st.Unlock()
}
last := atomic.LoadInt64(pLast)
if now-last < _degradeInterval {
return false
}
return atomic.CompareAndSwapInt64(pLast, last, now)
}

View File

@@ -0,0 +1,77 @@
package cache_test
import (
"time"
"go-common/library/cache/memcache"
"go-common/library/container/pool"
"go-common/library/ecode"
"go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/cache"
"go-common/library/net/http/blademaster/middleware/cache/store"
xtime "go-common/library/time"
"github.com/pkg/errors"
)
// This example create a cache middleware instance and two cache policy,
// then attach them to the specified path.
//
// The `PageCache` policy will attempt to cache the whole response by URI.
// It usually used to cache the common response.
//
// The `Degrader` policy usually used to prevent the API totaly unavailable if any disaster is happen.
// A succeeded response will be cached per 600s.
// The cache key is generated by specified args and its values.
// You can using file or memcache as cache backend for degradation currently.
//
// The `Cache` policy is used to work with multilevel HTTP caching architecture.
// This will cause client side response caching.
// We only support weak validator with `ETag` header currently.
func Example() {
mc := store.NewMemcache(&memcache.Config{
Config: &pool.Config{
Active: 10,
Idle: 2,
IdleTimeout: xtime.Duration(time.Second),
},
Name: "test",
Proto: "tcp",
Addr: "172.16.33.54:11211",
DialTimeout: xtime.Duration(time.Second),
ReadTimeout: xtime.Duration(time.Second),
WriteTimeout: xtime.Duration(time.Second),
})
ca := cache.New(mc)
deg := cache.NewDegrader(10)
pc := cache.NewPage(10)
ctl := cache.NewControl(10)
filter := func(ctx *blademaster.Context) bool {
if ctx.Request.Form.Get("cache") == "false" {
return false
}
return true
}
engine := blademaster.Default()
engine.GET("/users/profile", ca.Cache(deg.Args("name", "age"), nil), func(c *blademaster.Context) {
values := c.Request.URL.Query()
name := values.Get("name")
age := values.Get("age")
err := errors.New("error from others") // error from other call
if err != nil {
// mark this response should be degraded
c.JSON(nil, ecode.Degrade)
return
}
c.JSON(map[string]string{"name": name, "age": age}, nil)
})
engine.GET("/users/index", ca.Cache(pc, nil), func(c *blademaster.Context) {
c.String(200, "%s", "Title: User")
})
engine.GET("/users/list", ca.Cache(ctl, filter), func(c *blademaster.Context) {
c.JSON([]string{"user1", "user2", "user3"}, nil)
})
engine.Run(":18080")
}

View File

@@ -0,0 +1,171 @@
package cache
import (
"bytes"
"crypto/sha1"
"io"
"net/http"
"net/url"
"sync"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/cache/store"
proto "github.com/gogo/protobuf/proto"
)
// consts for blademaster cache
const (
_pagePrefix = "bm.page"
)
// Page is used to cache common response
type Page struct {
Expire int32
pool sync.Pool
}
type cachedWriter struct {
ctx *bm.Context
response http.ResponseWriter
store store.Store
status int
expire int32
key string
}
var _ http.ResponseWriter = &cachedWriter{}
// NewPage will create a new page cache struct
func NewPage(expire int32) *Page {
pc := &Page{
Expire: expire,
}
pc.pool.New = func() interface{} {
return &cachedWriter{}
}
return pc
}
// Key is used to identify response cache key in most key-value store
func (p *Page) Key(ctx *bm.Context) string {
url := ctx.Request.URL
key := urlEscape(_pagePrefix, url.RequestURI())
return key
}
// Handler is used to execute cache service
func (p *Page) Handler(store store.Store) bm.HandlerFunc {
return func(ctx *bm.Context) {
var (
resp *ResponseCache
cached []byte
err error
)
key := p.Key(ctx)
cached, err = store.Get(ctx, key)
// if we did got the previous cache,
// try to unmarshal it
if err == nil && len(cached) > 0 {
resp = new(ResponseCache)
err = proto.Unmarshal(cached, resp)
}
// if we failed to fetch the cache or failed to parse cached data,
// then consider try to cache this response
if err != nil || resp == nil {
writer := p.pool.Get().(*cachedWriter)
writer.ctx = ctx
writer.response = ctx.Writer
writer.key = key
writer.expire = p.Expire
writer.store = store
ctx.Writer = writer
ctx.Next()
p.pool.Put(writer)
return
}
// write cached response
headers := ctx.Writer.Header()
for key, value := range resp.Header {
headers[key] = value.Value
}
ctx.Writer.WriteHeader(int(resp.Status))
ctx.Writer.Write(resp.Data)
ctx.Abort()
}
}
func (w *cachedWriter) Header() http.Header {
return w.response.Header()
}
func (w *cachedWriter) WriteHeader(code int) {
w.status = int(code)
w.response.WriteHeader(code)
}
func (w *cachedWriter) Write(data []byte) (size int, err error) {
var (
origin []byte
pdata []byte
)
if size, err = w.response.Write(data); err != nil {
return
}
store := w.store
origin, err = store.Get(w.ctx, w.key)
resp := new(ResponseCache)
if err == nil || len(origin) > 0 {
err1 := proto.Unmarshal(origin, resp)
if err1 == nil {
data = append(resp.Data, data...)
}
}
resp.Status = int32(w.status)
resp.Header = headerValues(w.Header())
resp.Data = data
if pdata, err = proto.Marshal(resp); err != nil {
// cannot happen
log.Error("Failed to marshal response to protobuf: %v", err)
return
}
if err = store.Set(w.ctx, w.key, pdata, w.expire); err != nil {
log.Error("Failed to set response cache: %v", err)
return
}
return
}
func headerValues(headers http.Header) map[string]*HeaderValue {
result := make(map[string]*HeaderValue, len(headers))
for key, values := range headers {
result[key] = &HeaderValue{
Value: values,
}
}
return result
}
func urlEscape(prefix string, u string) string {
key := url.QueryEscape(u)
if len(key) > 200 {
h := sha1.New()
io.WriteString(h, u)
key = string(h.Sum(nil))
}
var buffer bytes.Buffer
buffer.WriteString(prefix)
buffer.WriteString(":")
buffer.WriteString(key)
return buffer.String()
}

View File

@@ -0,0 +1,104 @@
// Code generated by protoc-gen-gogo. DO NOT EDIT.
// source: page.proto
/*
Package cache is a generated protocol buffer package.
It is generated from these files:
page.proto
It has these top-level messages:
ResponseCache
HeaderValue
*/
package cache
import proto "github.com/gogo/protobuf/proto"
import fmt "fmt"
import math "math"
import _ "github.com/gogo/protobuf/gogoproto"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
type ResponseCache struct {
Status int32 `protobuf:"varint,1,opt,name=Status,proto3" json:"Status,omitempty"`
Header map[string]*HeaderValue `protobuf:"bytes,2,rep,name=Header" json:"Header,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value"`
Data []byte `protobuf:"bytes,3,opt,name=Data,proto3" json:"Data,omitempty"`
}
func (m *ResponseCache) Reset() { *m = ResponseCache{} }
func (m *ResponseCache) String() string { return proto.CompactTextString(m) }
func (*ResponseCache) ProtoMessage() {}
func (*ResponseCache) Descriptor() ([]byte, []int) { return fileDescriptorPage, []int{0} }
func (m *ResponseCache) GetStatus() int32 {
if m != nil {
return m.Status
}
return 0
}
func (m *ResponseCache) GetHeader() map[string]*HeaderValue {
if m != nil {
return m.Header
}
return nil
}
func (m *ResponseCache) GetData() []byte {
if m != nil {
return m.Data
}
return nil
}
type HeaderValue struct {
Value []string `protobuf:"bytes,1,rep,name=Value" json:"Value,omitempty"`
}
func (m *HeaderValue) Reset() { *m = HeaderValue{} }
func (m *HeaderValue) String() string { return proto.CompactTextString(m) }
func (*HeaderValue) ProtoMessage() {}
func (*HeaderValue) Descriptor() ([]byte, []int) { return fileDescriptorPage, []int{1} }
func (m *HeaderValue) GetValue() []string {
if m != nil {
return m.Value
}
return nil
}
func init() {
proto.RegisterType((*ResponseCache)(nil), "cache.responseCache")
proto.RegisterType((*HeaderValue)(nil), "cache.headerValue")
}
func init() { proto.RegisterFile("page.proto", fileDescriptorPage) }
var fileDescriptorPage = []byte{
// 231 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x54, 0x8f, 0x41, 0x4b, 0xc4, 0x30,
0x10, 0x85, 0x49, 0x6b, 0x0b, 0x3b, 0x55, 0x90, 0x41, 0x24, 0xec, 0x29, 0xac, 0x97, 0x5c, 0xcc,
0xc2, 0x7a, 0x59, 0xbc, 0xaa, 0xe0, 0xc5, 0x4b, 0x04, 0xef, 0x69, 0x1d, 0x5b, 0x51, 0x37, 0xa5,
0x4d, 0x84, 0xfd, 0x7f, 0xfe, 0x30, 0xe9, 0xa4, 0x87, 0xee, 0xed, 0x3d, 0xde, 0xf7, 0xe6, 0x31,
0x00, 0xbd, 0x6b, 0xc9, 0xf4, 0x83, 0x0f, 0x1e, 0x8b, 0xc6, 0x35, 0x1d, 0xad, 0x6f, 0xdb, 0xcf,
0xd0, 0xc5, 0xda, 0x34, 0xfe, 0x67, 0xdb, 0xfa, 0xd6, 0x6f, 0x39, 0xad, 0xe3, 0x07, 0x3b, 0x36,
0xac, 0x52, 0x6b, 0xf3, 0x27, 0xe0, 0x62, 0xa0, 0xb1, 0xf7, 0x87, 0x91, 0x1e, 0xa6, 0x03, 0x78,
0x0d, 0xe5, 0x6b, 0x70, 0x21, 0x8e, 0x52, 0x28, 0xa1, 0x0b, 0x3b, 0x3b, 0xdc, 0x43, 0xf9, 0x4c,
0xee, 0x9d, 0x06, 0x99, 0xa9, 0x5c, 0x57, 0x3b, 0x65, 0x78, 0xd0, 0x9c, 0xb4, 0x4d, 0x42, 0x9e,
0x0e, 0x61, 0x38, 0xda, 0x99, 0x47, 0x84, 0xb3, 0x47, 0x17, 0x9c, 0xcc, 0x95, 0xd0, 0xe7, 0x96,
0xf5, 0xfa, 0x05, 0xaa, 0x05, 0x8a, 0x97, 0x90, 0x7f, 0xd1, 0x91, 0x17, 0x57, 0x76, 0x92, 0xa8,
0xa1, 0xf8, 0x75, 0xdf, 0x91, 0x64, 0xa6, 0x84, 0xae, 0x76, 0x38, 0xaf, 0x75, 0x5c, 0x7a, 0x9b,
0x12, 0x9b, 0x80, 0xfb, 0x6c, 0x2f, 0x36, 0x37, 0x50, 0x2d, 0x12, 0xbc, 0x82, 0x82, 0x85, 0x14,
0x2a, 0xd7, 0x2b, 0x9b, 0x4c, 0x5d, 0xf2, 0xcb, 0x77, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff, 0x6f,
0x05, 0xcf, 0xb0, 0x36, 0x01, 0x00, 0x00,
}

View File

@@ -0,0 +1,13 @@
syntax = "proto3";
package cache;
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
message responseCache {
int32 Status = 1;
map<string, headerValue> Header = 2;
bytes Data = 3;
}
message headerValue {
repeated string Value = 1;
}

View File

@@ -0,0 +1,37 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"file.go",
"memcache.go",
"store.go",
],
importpath = "go-common/library/net/http/blademaster/middleware/cache/store",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/cache/memcache:go_default_library",
"//library/log:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,65 @@
package store
import (
"context"
"io/ioutil"
"os"
"path"
"go-common/library/log"
"github.com/pkg/errors"
)
// FileConfig config of File.
type FileConfig struct {
RootDir string
}
// File is a degrade file service.
type File struct {
c *FileConfig
}
var _ Store = &File{}
// NewFile new a file degrade service.
func NewFile(fc *FileConfig) *File {
if fc == nil {
panic(errors.New("file config is nil"))
}
fs := &File{c: fc}
if err := os.MkdirAll(fs.c.RootDir, 0755); err != nil {
panic(errors.Wrapf(err, "dir: %s", fs.c.RootDir))
}
return fs
}
// Set save the result of location to file.
// expire is not implemented in file storage.
func (fs *File) Set(ctx context.Context, key string, bs []byte, _ int32) (err error) {
file := path.Join(fs.c.RootDir, key)
tmp := file + ".tmp"
if err = ioutil.WriteFile(tmp, bs, 0644); err != nil {
log.Error("ioutil.WriteFile(%s, bs, 0644): error(%v)", tmp, err)
err = errors.Wrapf(err, "key: %s", key)
return
}
if err = os.Rename(tmp, file); err != nil {
log.Error("os.Rename(%s, %s): error(%v)", tmp, file, err)
err = errors.Wrapf(err, "key: %s", key)
return
}
return
}
// Get get result from file by locaiton+params.
func (fs *File) Get(ctx context.Context, key string) (bs []byte, err error) {
p := path.Join(fs.c.RootDir, key)
if bs, err = ioutil.ReadFile(p); err != nil {
log.Error("ioutil.ReadFile(%s): error(%v)", p, err)
err = errors.Wrapf(err, "key: %s", key)
return
}
return
}

View File

@@ -0,0 +1,54 @@
package store
import (
"context"
"go-common/library/cache/memcache"
"go-common/library/log"
)
// Memcache represents the cache with memcached persistence
type Memcache struct {
pool *memcache.Pool
}
// NewMemcache new a memcache store.
func NewMemcache(c *memcache.Config) *Memcache {
if c == nil {
panic("cache config is nil")
}
return &Memcache{
pool: memcache.NewPool(c),
}
}
// Set save the result to memcache store.
func (ms *Memcache) Set(ctx context.Context, key string, value []byte, expire int32) (err error) {
item := &memcache.Item{
Key: key,
Value: value,
Expiration: expire,
}
conn := ms.pool.Get(ctx)
defer conn.Close()
if err = conn.Set(item); err != nil {
log.Error("conn.Set(%s) error(%v)", key, err)
}
return
}
// Get get result from mc by locaiton+params.
func (ms *Memcache) Get(ctx context.Context, key string) ([]byte, error) {
conn := ms.pool.Get(ctx)
defer conn.Close()
r, err := conn.Get(key)
if err != nil {
if err == memcache.ErrNotFound {
//ignore not found error
return nil, nil
}
log.Error("conn.Get(%s) error(%v)", key, err)
return nil, err
}
return r.Value, nil
}

View File

@@ -0,0 +1,15 @@
package store
import (
"context"
)
// Store is the interface of a cache backend
type Store interface {
// Get retrieves an item from the cache. Returns the item or nil, and a bool indicating
// whether the key was found.
Get(ctx context.Context, key string) ([]byte, error)
// Set sets an item to the cache, replacing any existing item.
Set(ctx context.Context, key string, value []byte, expire int32) error
}

View File

@@ -0,0 +1,55 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["aqm_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = ["aqm.go"],
importpath = "go-common/library/net/http/blademaster/middleware/limit/aqm",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/container/queue/aqm:go_default_library",
"//library/ecode:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/rate:go_default_library",
"//library/rate/limit:go_default_library",
"//library/stat/prom:go_default_library",
],
)
go_test(
name = "go_default_xtest",
srcs = ["example_test.go"],
tags = ["automanaged"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,5 @@
### business/blademaster/supervisor
##### Version 1.0.0
1. 完成基本功能与测试

View File

@@ -0,0 +1,5 @@
# Author
lintnaghui
# Reviewer
maojian

View File

@@ -0,0 +1,7 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- lintnaghui
reviewers:
- lintnaghui
- maojian

View File

@@ -0,0 +1,13 @@
#### business/blademaster/supervisor
##### 项目简介
blademaster 的 aqm middleware主动队列管理请求延迟检测于优先级策略管理
##### 编译环境
- **请只用 Golang v1.8.x 以上版本编译执行**
##### 依赖包
- No other dependency

View File

@@ -0,0 +1,54 @@
package aqm
import (
"context"
"go-common/library/container/queue/aqm"
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
"go-common/library/rate"
"go-common/library/rate/limit"
"go-common/library/stat/prom"
)
const (
_family = "blademaster"
)
var (
stats = prom.New().WithState("go_active_queue_mng", []string{"family", "title"})
)
// AQM aqm midleware.
type AQM struct {
limiter rate.Limiter
}
// New return a ratelimit midleware.
func New(conf *aqm.Config) (s *AQM) {
return &AQM{
limiter: limit.New(conf),
}
}
// Limit return a bm handler func.
func (a *AQM) Limit() bm.HandlerFunc {
return func(c *bm.Context) {
done, err := a.limiter.Allow(c)
if err != nil {
stats.Incr(_family, c.Request.URL.Path[1:])
// TODO: priority request.
// c.JSON(nil, err)
// c.Abort()
return
}
defer func() {
if c.Error != nil && !ecode.Deadline.Equal(c.Error) && c.Err() != context.DeadlineExceeded {
done(rate.Ignore)
return
}
done(rate.Success)
}()
c.Next()
}
}

View File

@@ -0,0 +1,47 @@
package aqm
import (
"fmt"
"math/rand"
"net/http"
"sync"
"sync/atomic"
"testing"
"time"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
)
func init() {
log.Init(nil)
}
func TestAQM(t *testing.T) {
var group sync.WaitGroup
rand.Seed(time.Now().Unix())
eng := bm.Default()
router := eng.Use(New(nil).Limit())
router.GET("/aqm", testaqm)
go eng.Run(":9999")
var errcount int64
for i := 0; i < 100; i++ {
group.Add(1)
go func() {
defer group.Done()
for j := 0; j < 300; j++ {
_, err := http.Get("http://127.0.0.1:9999/aqm")
if err != nil {
atomic.AddInt64(&errcount, 1)
}
}
}()
}
group.Wait()
fmt.Println("errcount", errcount)
}
func testaqm(ctx *bm.Context) {
count := rand.Intn(100)
time.Sleep(time.Millisecond * time.Duration(count))
}

View File

@@ -0,0 +1,9 @@
package aqm_test
// This example create a supervisor middleware instance and attach to a blademaster engine,
// it will allow '/ping' API can be requested with specified policy.
// This example will block all http method except `GET` on '/ping' API in next hour,
// and allow in further.
func Example() {
}

View File

@@ -0,0 +1,74 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["permit_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//library/cache/memcache:go_default_library",
"//library/container/pool:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/netutil/breaker:go_default_library",
"//library/time:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"permit.go",
"session.go",
],
importpath = "go-common/library/net/http/blademaster/middleware/permit",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/admin/main/manager/api:go_default_library",
"//library/cache/memcache:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/metadata:go_default_library",
"//library/net/rpc/warden:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
],
)
go_test(
name = "go_default_xtest",
srcs = ["example_test.go"],
tags = ["automanaged"],
deps = [
"//library/cache/memcache:go_default_library",
"//library/container/pool:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/permit:go_default_library",
"//library/net/metadata:go_default_library",
"//library/net/netutil/breaker:go_default_library",
"//library/time:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,24 @@
### business/blademaster/permit
### Version 1.0.6
1. fix manager用户不存在的情况
### Version 1.0.5
1. permit无配置化
### Version 1.0.4
1. auth添加默认配置
### Version 1.0.3
1. auth去掉写cookie操作
##### Version 1.0.2
1. 修复dashboard auth cookie校验username问题
##### Version 1.0.1
1. auth后将username写入context
##### Version 1.0.0
1. 使用 dashbaord 来认证用户使用manager来获取用户权限使用 memcache 来缓存用户Session
2. 完成基本功能与测试

View File

@@ -0,0 +1,13 @@
#### business/blademaster/identify
##### 项目简介
blademaster 的 auth middleware主要用于设置路由的后台管理系统的登陆验证和鉴权策略
##### 编译环境
- **请只用 Golang v1.8.x 以上版本编译执行**
##### 依赖包
- No other dependency

View File

@@ -0,0 +1,101 @@
package permit_test
import (
"fmt"
"time"
"go-common/library/cache/memcache"
"go-common/library/container/pool"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/permit"
"go-common/library/net/metadata"
"go-common/library/net/netutil/breaker"
xtime "go-common/library/time"
)
// This example create a permit middleware instance and attach to several path,
// it will validate request by specified policy and put extra information into context. e.g., `uid`.
// It provides additional handler functions to provide the identification for your business handler.
func Example() {
a := permit.New(&permit.Config{
DsHTTPClient: &bm.ClientConfig{
App: &bm.App{
Key: "manager-go",
Secret: "949bbb2dd3178252638c2407578bc7ad",
},
Dial: xtime.Duration(time.Second),
Timeout: xtime.Duration(time.Second),
KeepAlive: xtime.Duration(time.Second * 10),
Breaker: &breaker.Config{
Window: xtime.Duration(time.Second),
Sleep: xtime.Duration(time.Millisecond * 100),
Bucket: 10,
Ratio: 0.5,
Request: 100,
},
},
MaHTTPClient: &bm.ClientConfig{
App: &bm.App{
Key: "f6433799dbd88751",
Secret: "36f8ddb1806207fe07013ab6a77a3935",
},
Dial: xtime.Duration(time.Second),
Timeout: xtime.Duration(time.Second),
KeepAlive: xtime.Duration(time.Second * 10),
Breaker: &breaker.Config{
Window: xtime.Duration(time.Second),
Sleep: xtime.Duration(time.Millisecond * 100),
Bucket: 10,
Ratio: 0.5,
Request: 100,
},
},
Session: &permit.SessionConfig{
SessionIDLength: 32,
CookieLifeTime: 1800,
CookieName: "mng-go",
Domain: ".bilibili.co",
Memcache: &memcache.Config{
Config: &pool.Config{
Active: 10,
Idle: 5,
IdleTimeout: xtime.Duration(time.Second * 80),
},
Name: "go-business/permit",
Proto: "tcp",
Addr: "172.16.33.54:11211",
DialTimeout: xtime.Duration(time.Millisecond * 1000),
ReadTimeout: xtime.Duration(time.Millisecond * 1000),
WriteTimeout: xtime.Duration(time.Millisecond * 1000),
},
},
ManagerHost: "http://uat-manager.bilibili.co",
DashboardHost: "http://uat-dashboard-mng.bilibili.co",
DashboardCaller: "manager-go",
})
p := permit.New2(nil)
e := bm.NewServer(nil)
//Check whether the user has logged in
e.GET("/login", a.Verify(), func(c *bm.Context) {
c.JSON("pass", nil)
})
//Check whether the user has logged in,and check th user has the access permisson to the specifed path
e.GET("/tag/del", a.Permit("TAG_DEL"), func(c *bm.Context) {
uid := metadata.Int64(c, metadata.Uid)
username := metadata.String(c, metadata.Username)
c.JSON(fmt.Sprintf("pass uid(%d) username(%s)", uid, username), nil)
})
e.GET("/check/login", p.Verify2(), func(c *bm.Context) {
c.JSON("pass", nil)
})
e.POST("/tag/del", p.Permit2("TAG_DEL"), func(c *bm.Context) {
uid := metadata.Int64(c, metadata.Uid)
username := metadata.String(c, metadata.Username)
c.JSON(fmt.Sprintf("pass uid(%d) username(%s)", uid, username), nil)
})
e.Run(":18080")
}

View File

@@ -0,0 +1,321 @@
package permit
import (
"net/url"
mng "go-common/app/admin/main/manager/api"
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/metadata"
"go-common/library/net/rpc/warden"
"github.com/pkg/errors"
)
const (
_verifyURI = "/api/session/verify"
_permissionURI = "/x/admin/manager/permission"
_sessIDKey = "_AJSESSIONID"
_sessUIDKey = "uid" // manager user_id
_sessUnKey = "username" // LDAP username
_defaultDomain = ".bilibili.co"
_defaultCookieName = "mng-go"
_defaultCookieLifeTime = 2592000
// CtxPermissions will be set into ctx.
CtxPermissions = "permissions"
)
// permissions .
type permissions struct {
UID int64 `json:"uid"`
Perms []string `json:"perms"`
}
// Permit is an auth middleware.
type Permit struct {
verifyURI string
permissionURI string
dashboardCaller string
dsClient *bm.Client // dashboard client
maClient *bm.Client // manager-admin client
sm *SessionManager // user Session
mng.PermitClient // mng grpc client
}
//Verify only export Verify function because of less configure
type Verify interface {
Verify() bm.HandlerFunc
}
// Config identify config.
type Config struct {
DsHTTPClient *bm.ClientConfig // dashboard client config. appkey can not reuse.
MaHTTPClient *bm.ClientConfig // manager-admin client config
Session *SessionConfig
ManagerHost string
DashboardHost string
DashboardCaller string
}
// Config2 .
type Config2 struct {
MngClient *warden.ClientConfig
Session *SessionConfig
}
// New new an auth service.
func New(c *Config) *Permit {
a := &Permit{
dashboardCaller: c.DashboardCaller,
verifyURI: c.DashboardHost + _verifyURI,
permissionURI: c.ManagerHost + _permissionURI,
dsClient: bm.NewClient(c.DsHTTPClient),
maClient: bm.NewClient(c.MaHTTPClient),
sm: newSessionManager(c.Session),
}
return a
}
// New2 .
func New2(c *warden.ClientConfig) *Permit {
permitClient, err := mng.NewClient(c)
if err != nil {
panic(errors.WithMessage(err, "Failed to dial mng rpc server"))
}
return &Permit{
PermitClient: permitClient,
sm: &SessionManager{},
}
}
// NewVerify new a verify service.
func NewVerify(c *Config) Verify {
a := &Permit{
verifyURI: c.DashboardHost + _verifyURI,
dsClient: bm.NewClient(c.DsHTTPClient),
dashboardCaller: c.DashboardCaller,
sm: newSessionManager(c.Session),
}
return a
}
// Verify2 check whether the user has logged in.
func (p *Permit) Verify2() bm.HandlerFunc {
return func(ctx *bm.Context) {
sid, username, err := p.login2(ctx)
if err != nil {
ctx.JSON(nil, ecode.Unauthorized)
ctx.Abort()
return
}
ctx.Set(_sessUnKey, username)
p.sm.setHTTPCookie(ctx, _defaultCookieName, sid)
}
}
// Verify return bm HandlerFunc which check whether the user has logged in.
func (p *Permit) Verify() bm.HandlerFunc {
return func(ctx *bm.Context) {
si, err := p.login(ctx)
if err != nil {
ctx.JSON(nil, ecode.Unauthorized)
ctx.Abort()
return
}
p.sm.SessionRelease(ctx, si)
}
}
// Permit return bm HandlerFunc which check whether the user has logged in and has the access permission of the location.
// If `permit` is empty,it will allow any logged in request.
func (p *Permit) Permit(permit string) bm.HandlerFunc {
return func(ctx *bm.Context) {
var (
si *Session
uid int64
perms []string
err error
)
si, err = p.login(ctx)
if err != nil {
ctx.JSON(nil, ecode.Unauthorized)
ctx.Abort()
return
}
defer p.sm.SessionRelease(ctx, si)
uid, perms, err = p.permissions(ctx, si.Get(_sessUnKey).(string))
if err == nil {
si.Set(_sessUIDKey, uid)
ctx.Set(_sessUIDKey, uid)
if md, ok := metadata.FromContext(ctx); ok {
md[metadata.Uid] = uid
}
}
if len(perms) > 0 {
ctx.Set(CtxPermissions, perms)
}
if !p.permit(permit, perms) {
ctx.JSON(nil, ecode.AccessDenied)
ctx.Abort()
return
}
}
}
// login check whether the user has logged in.
func (p *Permit) login(ctx *bm.Context) (si *Session, err error) {
si = p.sm.SessionStart(ctx)
if si.Get(_sessUnKey) == nil {
var username string
if username, err = p.verify(ctx); err != nil {
return
}
si.Set(_sessUnKey, username)
}
ctx.Set(_sessUnKey, si.Get(_sessUnKey))
if md, ok := metadata.FromContext(ctx); ok {
md[metadata.Username] = si.Get(_sessUnKey)
}
return
}
// Permit2 same function as permit function but reply on grpc.
func (p *Permit) Permit2(permit string) bm.HandlerFunc {
return func(ctx *bm.Context) {
sid, username, err := p.login2(ctx)
if err != nil {
ctx.JSON(nil, ecode.Unauthorized)
ctx.Abort()
return
}
p.sm.setHTTPCookie(ctx, _defaultCookieName, sid)
ctx.Set(_sessUnKey, username)
if md, ok := metadata.FromContext(ctx); ok {
md[metadata.Username] = username
}
reply, err := p.Permissions(ctx, &mng.PermissionReq{Username: username})
if err != nil {
if ecode.NothingFound.Equal(err) && permit != "" {
ctx.JSON(nil, ecode.AccessDenied)
ctx.Abort()
}
return
}
ctx.Set(_sessUIDKey, reply.Uid)
if md, ok := metadata.FromContext(ctx); ok {
md[metadata.Uid] = reply.Uid
}
if len(reply.Perms) > 0 {
ctx.Set(CtxPermissions, reply.Perms)
}
if !p.permit(permit, reply.Perms) {
ctx.JSON(nil, ecode.AccessDenied)
ctx.Abort()
return
}
}
}
// login2 .
func (p *Permit) login2(ctx *bm.Context) (sid, uname string, err error) {
var dsbsid, mngsid string
dsbck, err := ctx.Request.Cookie(_sessIDKey)
if err == nil {
dsbsid = dsbck.Value
}
if dsbsid == "" {
err = ecode.Unauthorized
return
}
mngck, err := ctx.Request.Cookie(_defaultCookieName)
if err == nil {
mngsid = mngck.Value
}
reply, err := p.Login(ctx, &mng.LoginReq{Mngsid: mngsid, Dsbsid: dsbsid})
if err != nil {
log.Error("mng rpc Login error(%v)", err)
return
}
sid = reply.Sid
uname = reply.Username
return
}
func (p *Permit) verify(ctx *bm.Context) (username string, err error) {
var (
sid string
r = ctx.Request
)
session, err := r.Cookie(_sessIDKey)
if err == nil {
sid = session.Value
}
if sid == "" {
err = ecode.Unauthorized
return
}
username, err = p.verifyDashboard(ctx, sid)
return
}
// permit check whether user has the access permission of the location.
func (p *Permit) permit(permit string, permissions []string) bool {
if permit == "" {
return true
}
for _, p := range permissions {
if p == permit {
// access the permit
return true
}
}
return false
}
// verifyDashboard check whether the user is valid from Dashboard.
func (p *Permit) verifyDashboard(ctx *bm.Context, sid string) (username string, err error) {
params := url.Values{}
params.Set("session_id", sid)
params.Set("encrypt", "md5")
params.Set("caller", p.dashboardCaller)
var res struct {
Code int `json:"code"`
UserName string `json:"username"`
}
if err = p.dsClient.Get(ctx, p.verifyURI, metadata.String(ctx, metadata.RemoteIP), params, &res); err != nil {
log.Error("dashboard get verify Session url(%s) error(%v)", p.verifyURI+"?"+params.Encode(), err)
return
}
if ecode.Int(res.Code) != ecode.OK {
log.Error("dashboard get verify Session url(%s) error(%v)", p.verifyURI+"?"+params.Encode(), res.Code)
err = ecode.Int(res.Code)
return
}
username = res.UserName
return
}
// permissions get user's permisssions from manager-admin.
func (p *Permit) permissions(ctx *bm.Context, username string) (uid int64, perms []string, err error) {
params := url.Values{}
params.Set(_sessUnKey, username)
var res struct {
Code int `json:"code"`
Data permissions `json:"data"`
}
if err = p.maClient.Get(ctx, p.permissionURI, metadata.String(ctx, metadata.RemoteIP), params, &res); err != nil {
log.Error("dashboard get permissions url(%s) error(%v)", p.permissionURI+"?"+params.Encode(), err)
return
}
if ecode.Int(res.Code) != ecode.OK {
log.Error("dashboard get permissions url(%s) error(%v)", p.permissionURI+"?"+params.Encode(), res.Code)
err = ecode.Int(res.Code)
return
}
perms = res.Data.Perms
uid = res.Data.UID
return
}

View File

@@ -0,0 +1,294 @@
package permit
import (
"context"
"net/http"
"net/url"
"sync"
"testing"
"time"
"go-common/library/cache/memcache"
"go-common/library/container/pool"
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/netutil/breaker"
xtime "go-common/library/time"
)
var (
once sync.Once
)
type Response struct {
Code int `json:"code"`
Data string `json:"data"`
}
func init() {
log.Init(nil)
}
func client() *bm.Client {
return bm.NewClient(&bm.ClientConfig{
App: &bm.App{
Key: "test",
Secret: "test",
},
Dial: xtime.Duration(time.Second),
Timeout: xtime.Duration(time.Second),
KeepAlive: xtime.Duration(time.Second * 10),
Breaker: &breaker.Config{
Window: xtime.Duration(time.Second),
Sleep: xtime.Duration(time.Millisecond * 100),
Bucket: 10,
Ratio: 0.5,
Request: 100,
},
})
}
func getPermit() *Permit {
return New(&Config{
DsHTTPClient: &bm.ClientConfig{
App: &bm.App{
Key: "manager-go",
Secret: "949bbb2dd3178252638c2407578bc7ad",
},
Dial: xtime.Duration(time.Second),
Timeout: xtime.Duration(time.Second),
KeepAlive: xtime.Duration(time.Second * 10),
Breaker: &breaker.Config{
Window: xtime.Duration(time.Second),
Sleep: xtime.Duration(time.Millisecond * 100),
Bucket: 10,
Ratio: 0.5,
Request: 100,
},
},
MaHTTPClient: &bm.ClientConfig{
App: &bm.App{
Key: "f6433799dbd88751",
Secret: "36f8ddb1806207fe07013ab6a77a3935",
},
Dial: xtime.Duration(time.Second),
Timeout: xtime.Duration(time.Second),
KeepAlive: xtime.Duration(time.Second * 10),
Breaker: &breaker.Config{
Window: xtime.Duration(time.Second),
Sleep: xtime.Duration(time.Millisecond * 100),
Bucket: 10,
Ratio: 0.5,
Request: 100,
},
},
Session: &SessionConfig{
SessionIDLength: 32,
CookieLifeTime: 1800,
CookieName: "mng-go",
Domain: ".bilibili.co",
Memcache: &memcache.Config{
Config: &pool.Config{
Active: 10,
Idle: 5,
IdleTimeout: xtime.Duration(time.Second * 80),
},
Name: "go-business/auth",
Proto: "tcp",
Addr: "172.16.33.54:11211",
DialTimeout: xtime.Duration(time.Millisecond * 1000),
ReadTimeout: xtime.Duration(time.Millisecond * 1000),
WriteTimeout: xtime.Duration(time.Millisecond * 1000),
},
},
ManagerHost: "http://uat-manager.bilibili.co",
DashboardHost: "http://dashboard-mng.bilibili.co",
DashboardCaller: "manager-go",
})
}
func engine() *bm.Engine {
e := bm.NewServer(nil)
a := getPermit()
e.GET("/login", a.Verify(), func(c *bm.Context) {
c.JSON("pass", nil)
})
e.GET("/tag/del", a.Permit("TAG_DEL"), func(c *bm.Context) {
c.JSON("pass", nil)
})
e.GET("/tag/admin", a.Permit("TAG_ADMIN"), func(c *bm.Context) {
c.JSON("pass", nil)
})
return e
}
func setSession(uid int64, username string) (string, error) {
a := getPermit()
sv := a.sm.newSession(context.TODO())
sv.Set("username", username)
mcConn := a.sm.mc.Get(context.TODO())
defer mcConn.Close()
key := sv.Sid
item := &memcache.Item{
Key: key,
Object: sv,
Flags: memcache.FlagJSON,
Expiration: int32(a.sm.c.CookieLifeTime),
}
if err := mcConn.Set(item); err != nil {
return "", err
}
return key, nil
}
func startEngine(t *testing.T) func() {
return func() {
e := engine()
err := e.Run(":18080")
if err != nil {
t.Fatalf("failed to run server!%v", err)
}
}
}
func TestLoginSuccess(t *testing.T) {
go once.Do(startEngine(t))
time.Sleep(time.Millisecond * 100)
sid, err := setSession(2233, "caoguoliang")
if err != nil {
t.Fatalf("faild to set session !err:=%v", err)
}
query := url.Values{}
query.Set("test", "test")
cli := client()
req, err := cli.NewRequest("GET", "http://127.0.0.1:18080/login", "", query)
if err != nil {
t.Fatalf("Failed to build request: %v", err)
}
req.AddCookie(&http.Cookie{
Name: "mng-go",
Value: sid,
})
req.AddCookie(&http.Cookie{
Name: "username",
Value: "caoguoliang",
})
req.AddCookie(&http.Cookie{
Name: "_AJSESSIONID",
Value: "87fa8450e93511e79ed8522233007f8a",
})
res := Response{}
if err := cli.Do(context.TODO(), req, &res); err != nil {
t.Fatalf("Failed to send request: %v", err)
}
if res.Code != 0 || res.Data != "pass" {
t.Fatalf("Unexpected response code(%d) data(%v)", res.Code, res.Data)
}
}
func TestLoginFail(t *testing.T) {
go once.Do(startEngine(t))
time.Sleep(time.Millisecond * 100)
query := url.Values{}
query.Set("test", "test")
cli := client()
req, err := cli.NewRequest("GET", "http://127.0.0.1:18080/login", "", query)
if err != nil {
t.Fatalf("Failed to build request: %v", err)
}
req.AddCookie(&http.Cookie{
Name: "mng-go",
Value: "fakesess",
})
req.AddCookie(&http.Cookie{
Name: "username",
Value: "caoguoliang",
})
req.AddCookie(&http.Cookie{
Name: "_AJSESSIONID",
Value: "testsess",
})
res := Response{}
if err := cli.Do(context.TODO(), req, &res); err != nil {
t.Fatalf("Failed to send request: %v", err)
}
if res.Code != ecode.Unauthorized.Code() {
t.Fatalf("This request should be forbidden: code(%d) data(%v)", res.Code, res.Data)
}
}
func TestVerifySuccess(t *testing.T) {
go once.Do(startEngine(t))
time.Sleep(time.Millisecond * 100)
sid, err := setSession(2233, "caoguoliang")
if err != nil {
t.Fatalf("faild to set session !err:=%v", err)
}
query := url.Values{}
query.Set("test", "test")
cli := client()
req, err := cli.NewRequest("GET", "http://127.0.0.1:18080/tag/del", "", query)
if err != nil {
t.Fatalf("Failed to build request: %v", err)
}
req.AddCookie(&http.Cookie{
Name: "mng-go",
Value: sid,
})
req.AddCookie(&http.Cookie{
Name: "username",
Value: "caoguoliang",
})
req.AddCookie(&http.Cookie{
Name: "_AJSESSIONID",
Value: "87fa8450e93511e79ed8522233007f8a",
})
res := Response{}
if err := cli.Do(context.TODO(), req, &res); err != nil {
t.Fatalf("Failed to send request: %v", err)
}
if res.Code != 0 || res.Data != "pass" {
t.Fatalf("Unexpected response code(%d) data(%v)", res.Code, res.Data)
}
}
func TestVerifyFail(t *testing.T) {
go once.Do(startEngine(t))
time.Sleep(time.Millisecond * 100)
sid, err := setSession(2233, "caoguoliang")
if err != nil {
t.Fatalf("faild to set session !err:=%v", err)
}
query := url.Values{}
query.Set("test", "test")
cli := client()
req, err := cli.NewRequest("GET", "http://127.0.0.1:18080/tag/admin", "", query)
if err != nil {
t.Fatalf("Failed to build request: %v", err)
}
req.AddCookie(&http.Cookie{
Name: "mng-go",
Value: sid,
})
req.AddCookie(&http.Cookie{
Name: "username",
Value: "caoguoliang",
})
req.AddCookie(&http.Cookie{
Name: "_AJSESSIONID",
Value: "87fa8450e93511e79ed8522233007f8a",
})
res := Response{}
if err := cli.Do(context.TODO(), req, &res); err != nil {
t.Fatalf("Failed to send request: %v", err)
}
if res.Code != ecode.AccessDenied.Code() {
t.Fatalf("This request should be forbidden: code(%d) data(%v)", res.Code, res.Data)
}
}

View File

@@ -0,0 +1,152 @@
package permit
import (
"context"
"crypto/rand"
"encoding/hex"
"net/http"
"net/url"
"sync"
"time"
"go-common/library/cache/memcache"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
)
// Session http session.
type Session struct {
Sid string
lock sync.RWMutex
Values map[string]interface{}
}
// SessionConfig config of Session.
type SessionConfig struct {
SessionIDLength int
CookieLifeTime int
CookieName string
Domain string
Memcache *memcache.Config
}
// SessionManager .
type SessionManager struct {
mc *memcache.Pool // Session cache
c *SessionConfig
}
// newSessionManager .
func newSessionManager(c *SessionConfig) (s *SessionManager) {
s = &SessionManager{
mc: memcache.NewPool(c.Memcache),
c: c,
}
return
}
// SessionStart start session.
func (s *SessionManager) SessionStart(ctx *bm.Context) (si *Session) {
// check manager Session id, if err or no exist need new one.
if si, _ = s.cache(ctx); si == nil {
si = s.newSession(ctx)
}
return
}
// SessionRelease flush session into store.
func (s *SessionManager) SessionRelease(ctx *bm.Context, sv *Session) {
// set http cookie
s.setHTTPCookie(ctx, s.c.CookieName, sv.Sid)
// set mc
conn := s.mc.Get(ctx)
defer conn.Close()
key := sv.Sid
item := &memcache.Item{
Key: key,
Object: sv,
Flags: memcache.FlagJSON,
Expiration: int32(s.c.CookieLifeTime),
}
if err := conn.Set(item); err != nil {
log.Error("SessionManager set error(%s,%v)", key, err)
}
}
// SessionDestroy destroy session.
func (s *SessionManager) SessionDestroy(ctx *bm.Context, sv *Session) {
conn := s.mc.Get(ctx)
defer conn.Close()
if err := conn.Delete(sv.Sid); err != nil {
log.Error("SessionManager delete error(%s,%v)", sv.Sid, err)
}
}
func (s *SessionManager) cache(ctx *bm.Context) (res *Session, err error) {
ck, err := ctx.Request.Cookie(s.c.CookieName)
if err != nil || ck == nil {
return
}
sid := ck.Value
// get from cache
conn := s.mc.Get(ctx)
defer conn.Close()
r, err := conn.Get(sid)
if err != nil {
if err == memcache.ErrNotFound {
err = nil
return
}
log.Error("conn.Get(%s) error(%v)", sid, err)
return
}
res = &Session{}
if err = conn.Scan(r, res); err != nil {
log.Error("conn.Scan(%v) error(%v)", string(r.Value), err)
}
return
}
func (s *SessionManager) newSession(ctx context.Context) (res *Session) {
b := make([]byte, s.c.SessionIDLength)
n, err := rand.Read(b)
if n != len(b) || err != nil {
return nil
}
res = &Session{
Sid: hex.EncodeToString(b),
Values: make(map[string]interface{}),
}
return
}
func (s *SessionManager) setHTTPCookie(ctx *bm.Context, name, value string) {
cookie := &http.Cookie{
Name: name,
Value: url.QueryEscape(value),
Path: "/",
HttpOnly: true,
Domain: _defaultDomain,
}
cookie.MaxAge = _defaultCookieLifeTime
cookie.Expires = time.Now().Add(time.Duration(_defaultCookieLifeTime) * time.Second)
http.SetCookie(ctx.Writer, cookie)
}
// Get get value by key.
func (s *Session) Get(key string) (value interface{}) {
s.lock.RLock()
defer s.lock.RUnlock()
value = s.Values[key]
return
}
// Set set value into session.
func (s *Session) Set(key string, value interface{}) (err error) {
s.lock.Lock()
defer s.lock.Unlock()
s.Values[key] = value
return
}

View File

@@ -0,0 +1,59 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["proxy_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = ["proxy.go"],
importpath = "go-common/library/net/http/blademaster/middleware/proxy",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/conf/env:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/metadata:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
],
)
go_test(
name = "go_default_xtest",
srcs = ["example_test.go"],
tags = ["automanaged"],
deps = [
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/proxy:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,5 @@
### business/blademaster/proxy
##### Version 1.0.0
1. 完成基本功能与测试

View File

@@ -0,0 +1,5 @@
# Author
zhoujiahui
# Reviewer
maojian

View File

@@ -0,0 +1,7 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- zhoujiahui
reviewers:
- maojian
- zhoujiahui

View File

@@ -0,0 +1,13 @@
#### business/blademaster/proxy
##### 项目简介
blademaster 的 reverse proxy middleware主要用于转发一些 API 请求
##### 编译环境
- **请只用 Golang v1.8.x 以上版本编译执行**
##### 依赖包
- No other dependency

View File

@@ -0,0 +1,49 @@
package proxy_test
import (
"go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/proxy"
)
// This example create several reverse proxy to show how to use proxy middleware.
// We proxy three path to `api.bilibili.com` and return response without any changes.
func Example() {
proxies := map[string]string{
"/index": "http://api.bilibili.com/html/index",
"/ping": "http://api.bilibili.com/api/ping",
"/api/versions": "http://api.bilibili.com/api/web/versions",
}
engine := blademaster.Default()
for path, ep := range proxies {
engine.GET(path, proxy.NewAlways(ep))
}
engine.Run(":18080")
}
// This example create several reverse proxy to show how to use jd proxy middleware.
// The request will be proxied to destination only when request is from specified datacenter.
func ExampleNewZoneProxy() {
proxies := map[string]string{
"/index": "http://api.bilibili.com/html/index",
"/ping": "http://api.bilibili.com/api/ping",
"/api/versions": "http://api.bilibili.com/api/web/versions",
}
engine := blademaster.Default()
// proxy to specified destination
for path, ep := range proxies {
engine.GET(path, proxy.NewZoneProxy("sh004", ep), func(ctx *blademaster.Context) {
ctx.String(200, "Origin")
})
}
// proxy with request path
ug := engine.Group("/update", proxy.NewZoneProxy("sh004", "http://sh001-api.bilibili.com"))
ug.POST("/name", func(ctx *blademaster.Context) {
ctx.String(500, "Should not be accessed")
})
ug.POST("/sign", func(ctx *blademaster.Context) {
ctx.String(500, "Should not be accessed")
})
engine.Run(":18080")
}

View File

@@ -0,0 +1,118 @@
package proxy
import (
"bytes"
"io"
"io/ioutil"
stdlog "log"
"net/http"
"net/http/httputil"
"net/url"
"go-common/library/conf/env"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/metadata"
"github.com/pkg/errors"
)
type endpoint struct {
url *url.URL
proxy *httputil.ReverseProxy
condition func(ctx *bm.Context) bool
}
type logger struct{}
func (logger) Write(p []byte) (int, error) {
log.Warn("%s", string(p))
return len(p), nil
}
func newep(rawurl string, condition func(ctx *bm.Context) bool) *endpoint {
u, err := url.Parse(rawurl)
if err != nil {
panic(errors.Errorf("Invalid URL: %s", rawurl))
}
e := &endpoint{
url: u,
}
e.proxy = &httputil.ReverseProxy{
Director: e.director,
ErrorLog: stdlog.New(logger{}, "bm.proxy: ", stdlog.LstdFlags),
}
e.condition = condition
return e
}
func (e *endpoint) director(req *http.Request) {
req.URL.Scheme = e.url.Scheme
req.URL.Host = e.url.Host
// keep the origin request path
if e.url.Path != "" {
req.URL.Path = e.url.Path
}
body, length := rebuildBody(req)
req.Body = body
req.ContentLength = int64(length)
}
func (e *endpoint) ServeHTTP(ctx *bm.Context) {
req := ctx.Request
ip := metadata.String(ctx, metadata.RemoteIP)
logArgs := []log.D{
log.KV("method", req.Method),
log.KV("ip", ip),
log.KV("path", req.URL.Path),
log.KV("params", req.Form.Encode()),
}
if !e.condition(ctx) {
logArgs = append(logArgs, log.KV("proxied", "false"))
log.Infov(ctx, logArgs...)
return
}
logArgs = append(logArgs, log.KV("proxied", "true"))
log.Infov(ctx, logArgs...)
e.proxy.ServeHTTP(ctx.Writer, ctx.Request)
ctx.Abort()
}
func rebuildBody(req *http.Request) (io.ReadCloser, int) {
// GET request
if req.Body == nil {
return nil, 0
}
// Submit with form
if len(req.PostForm) > 0 {
br := bytes.NewReader([]byte(req.PostForm.Encode()))
return ioutil.NopCloser(br), br.Len()
}
// copy the original body
bodyBytes, _ := ioutil.ReadAll(req.Body)
br := bytes.NewReader(bodyBytes)
return ioutil.NopCloser(br), br.Len()
}
func always(ctx *bm.Context) bool {
return true
}
// NewZoneProxy is
func NewZoneProxy(matchZone, dst string) bm.HandlerFunc {
ep := newep(dst, func(*bm.Context) bool {
if env.Zone == matchZone {
return true
}
return false
})
return ep.ServeHTTP
}
// NewAlways is
func NewAlways(dst string) bm.HandlerFunc {
ep := newep(dst, always)
return ep.ServeHTTP
}

View File

@@ -0,0 +1,170 @@
package proxy
import (
"bytes"
"context"
"net/http"
"net/url"
"sync"
"testing"
"time"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"github.com/stretchr/testify/assert"
)
func init() {
log.Init(nil)
}
func TestProxy(t *testing.T) {
engine := bm.Default()
engine.GET("/icon", NewAlways("http://api.bilibili.com/x/web-interface/index/icon"))
engine.POST("/x/web-interface/archive/like", NewAlways("http://api.bilibili.com"))
go engine.Run(":18080")
defer func() {
engine.Server().Shutdown(context.TODO())
}()
time.Sleep(time.Second)
req, err := http.NewRequest("GET", "http://127.0.0.1:18080/icon", nil)
assert.NoError(t, err)
req.Host = "api.bilibili.com"
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
// proxy form request
form := url.Values{}
form.Set("arg1", "1")
form.Set("arg2", "2")
req, err = http.NewRequest("POST", "http://127.0.0.1:18080/x/web-interface/archive/like?param=test", bytes.NewReader([]byte(form.Encode())))
assert.NoError(t, err)
req.Host = "api.bilibili.com"
resp, err = http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
// proxy json request
bs := []byte(`{"arg1": 1, "arg2": 2}`)
req, err = http.NewRequest("POST", "http://127.0.0.1:18080/x/web-interface/archive/like?param=test", bytes.NewReader(bs))
assert.NoError(t, err)
req.Host = "api.bilibili.com"
req.Header.Set("Content-Type", "application/json; charset=utf-8")
resp, err = http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
}
func TestProxyRace(t *testing.T) {
engine := bm.Default()
engine.GET("/icon", NewAlways("http://api.bilibili.com/x/web-interface/index/icon"))
go engine.Run(":18080")
defer func() {
engine.Server().Shutdown(context.TODO())
}()
time.Sleep(time.Second)
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
req, err := http.NewRequest("GET", "http://127.0.0.1:18080/icon", nil)
assert.NoError(t, err)
req.Host = "api.bilibili.com"
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
}()
}
wg.Wait()
}
func TestZoneProxy(t *testing.T) {
engine := bm.Default()
engine.GET("/icon", NewZoneProxy("sh004", "http://api.bilibili.com/x/web-interface/index/icon"), func(ctx *bm.Context) {
ctx.AbortWithStatus(500)
})
engine.GET("/icon2", NewZoneProxy("none", "http://api.bilibili.com/x/web-interface/index/icon2"), func(ctx *bm.Context) {
ctx.AbortWithStatus(200)
})
ug := engine.Group("/update", NewZoneProxy("sh004", "http://api.bilibili.com"))
ug.POST("/name", func(ctx *bm.Context) {
ctx.AbortWithStatus(500)
})
ug.POST("/sign", func(ctx *bm.Context) {
ctx.AbortWithStatus(500)
})
go engine.Run(":18080")
defer func() {
engine.Server().Shutdown(context.TODO())
}()
time.Sleep(time.Second)
req, err := http.NewRequest("GET", "http://127.0.0.1:18080/icon", nil)
assert.NoError(t, err)
req.Host = "api.bilibili.com"
req.Header.Set("X-BILI-SLB", "shjd-out-slb")
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
req.URL.Path = "/icon2"
resp, err = http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
req.URL.Path = "/update/name"
resp, err = http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
req.URL.Path = "/update/sign"
resp, err = http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
}
func BenchmarkProxy(b *testing.B) {
engine := bm.Default()
engine.GET("/icon", NewAlways("http://api.bilibili.com/x/web-interface/index/icon"))
go engine.Run(":18080")
defer func() {
engine.Server().Shutdown(context.TODO())
}()
time.Sleep(time.Second)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
req, err := http.NewRequest("GET", "http://127.0.0.1:18080/icon", nil)
assert.NoError(b, err)
req.Host = "api.bilibili.com"
resp, err := http.DefaultClient.Do(req)
assert.NoError(b, err)
defer resp.Body.Close()
assert.Equal(b, 200, resp.StatusCode)
}
})
}

View File

@@ -0,0 +1,53 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["limit_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = ["//library/net/http/blademaster:go_default_library"],
)
go_library(
name = "go_default_library",
srcs = ["limit.go"],
importpath = "go-common/library/net/http/blademaster/middleware/rate",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//vendor/golang.org/x/time/rate:go_default_library",
],
)
go_test(
name = "go_default_xtest",
srcs = ["example_test.go"],
tags = ["automanaged"],
deps = [
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/rate:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,5 @@
### business/blademaster/rate
##### Version 1.0.0
1. 完成基本功能与测试

View File

@@ -0,0 +1,6 @@
# Author
lintnaghui
caoguoliang
# Reviewer
maojian

View File

@@ -0,0 +1,9 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- caoguoliang
- lintnaghui
reviewers:
- caoguoliang
- lintnaghui
- maojian

View File

@@ -0,0 +1,13 @@
#### business/blademaster/rate
##### 项目简介
blademaster 的 rate middleware主要用于限制内部调用的频率
##### 编译环境
- **请只用 Golang v1.8.x 以上版本编译执行**
##### 依赖包
- No other dependency

View File

@@ -0,0 +1,28 @@
package rate_test
import (
"go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/rate"
)
// This example create a rate middleware instance and attach to a blademaster engine,
// it will protect '/ping' API frequency with specified policy.
// If any internal service who requests this API more frequently than 1 req/second,
// a StatusTooManyRequests error will be raised.
func Example() {
lim := rate.New(&rate.Config{
URLs: map[string]*rate.Limit{
"/ping": &rate.Limit{Limit: 1, Burst: 2},
},
Apps: map[string]*rate.Limit{
"a-secret-app-key": &rate.Limit{Limit: 1, Burst: 2},
},
})
engine := blademaster.Default()
engine.Use(lim)
engine.GET("/ping", func(c *blademaster.Context) {
c.String(200, "%s", "pong")
})
engine.Run(":18080")
}

View File

@@ -0,0 +1,137 @@
package rate
import (
"net/http"
"sync/atomic"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"golang.org/x/time/rate"
)
const (
_defBurst = 100
)
// Limiter controls how frequently events are allowed to happen.
type Limiter struct {
apps atomic.Value
urls atomic.Value
}
// Config limitter conf.
type Config struct {
Apps map[string]*Limit
URLs map[string]*Limit
}
// Limit limit conf.
type Limit struct {
Limit rate.Limit
Burst int
}
// New return Limiter.
func New(conf *Config) (l *Limiter) {
l = &Limiter{}
l.apps.Store(make(map[string]*rate.Limiter))
l.urls.Store(make(map[string]*rate.Limiter))
if conf != nil {
l.Reload(conf)
}
return
}
// Reload reload limit conf.
func (l *Limiter) Reload(c *Config) {
if c == nil {
return
}
var (
ok bool
al *rate.Limiter
ul *rate.Limiter
as map[string]*rate.Limiter
nas map[string]*rate.Limiter
us map[string]*rate.Limiter
nus map[string]*rate.Limiter
)
if as, ok = l.apps.Load().(map[string]*rate.Limiter); !ok {
log.Error("apps limiter load map hava no data ")
return
}
nas = make(map[string]*rate.Limiter, len(as))
for k, v := range as {
nas[k] = v
}
for k, v := range c.Apps {
if al, ok = nas[k]; !ok || (al.Burst() != v.Burst || al.Limit() != v.Limit) {
nas[k] = rate.NewLimiter(v.fix())
}
}
l.apps.Store(nas)
if us, ok = l.urls.Load().(map[string]*rate.Limiter); !ok {
log.Error("urls limiter load map hava no data ")
return
}
nus = make(map[string]*rate.Limiter, len(us))
for k, v := range us {
nus[k] = v
}
for k, v := range c.URLs {
if ul, ok = nus[k]; !ok || (ul.Burst() != v.Burst || ul.Limit() != v.Limit) {
nus[k] = rate.NewLimiter(v.fix())
}
}
l.urls.Store(nus)
}
func (l *Limit) fix() (lim rate.Limit, b int) {
lim = rate.Inf
b = _defBurst
if l.Limit <= 0 {
lim = rate.Inf
} else {
lim = l.Limit
}
if l.Burst > 0 {
b = l.Burst
}
return
}
// Allow reports whether event may happen at time now.
func (l *Limiter) Allow(appKey, path string) bool {
if as, ok := l.apps.Load().(map[string]*rate.Limiter); ok {
if lim, ok := as[appKey]; ok {
if !lim.Allow() {
return false
}
}
}
if us, ok := l.urls.Load().(map[string]*rate.Limiter); ok {
if lim, ok := us[path]; ok {
if !lim.Allow() {
return false
}
}
}
return true
}
func (l *Limiter) ServeHTTP(c *bm.Context) {
req := c.Request
appkey := req.Form.Get("appkey")
path := req.URL.Path
if !l.Allow(appkey, path) {
c.AbortWithStatus(http.StatusTooManyRequests)
return
}
}
// Handler is router allow handle.
func (l *Limiter) Handler() bm.HandlerFunc {
return l.ServeHTTP
}

View File

@@ -0,0 +1,131 @@
package rate
import (
"context"
"io/ioutil"
"net/http"
"testing"
"time"
bm "go-common/library/net/http/blademaster"
)
func TestLimiterUrl(t *testing.T) {
l := New(&Config{
URLs: map[string]*Limit{"/limit/test": &Limit{Limit: 1, Burst: 2}},
})
if !l.Allow("testApp", "/limit/test") {
t.Logf("request should pass,but blocked")
t.FailNow()
}
if !l.Allow("testApp", "/limit/test") {
t.Logf("request should pass,but blocked")
t.FailNow()
}
if l.Allow("testApp", "/limit/test") {
t.Logf("request should block,but passed")
t.FailNow()
}
}
func TestLimiterApp(t *testing.T) {
l := New(&Config{
Apps: map[string]*Limit{"testApp": &Limit{Limit: 1, Burst: 2}},
})
if !l.Allow("testApp", "/limit/test") {
t.Logf("request should pass,but blocked")
t.FailNow()
}
if !l.Allow("testApp", "/limit/test") {
t.Logf("request should pass,but blocked")
t.FailNow()
}
if l.Allow("testApp", "/limit/test") {
t.Logf("request should block,but passed")
t.FailNow()
}
}
func TestLimiterUrlApp(t *testing.T) {
l := New(&Config{
Apps: map[string]*Limit{"testApp": &Limit{Limit: 2, Burst: 1}},
URLs: map[string]*Limit{"/limit/test": &Limit{Limit: 2, Burst: 1}},
})
if !l.Allow("testApp", "/limit/test") {
t.Logf("request should pass,but blocked")
t.FailNow()
}
if l.Allow("testApp", "/limit/test") {
t.Logf("request should block,but passed")
t.FailNow()
}
l.Reload(&Config{
Apps: map[string]*Limit{"testApp": &Limit{Limit: 1, Burst: 2}},
URLs: map[string]*Limit{"/limit/test": &Limit{Limit: 1, Burst: 2}},
})
if !l.Allow("testApp", "/limit/test") {
t.Logf("request should pass,but blocked")
t.FailNow()
}
if !l.Allow("testApp", "/limit/test") {
t.Logf("request should pass,but blocked")
t.FailNow()
}
if l.Allow("testApp", "/limit/test") {
t.Logf("request should block,but passed")
t.FailNow()
}
}
func TestLimiterHandler(t *testing.T) {
l := New(&Config{
Apps: map[string]*Limit{"testApp": &Limit{Limit: 1, Burst: 1}},
URLs: map[string]*Limit{"/limit/test": &Limit{Limit: 2, Burst: 4}},
})
engine := bm.New()
engine.Use(l.Handler())
engine.GET("/limit/test", func(c *bm.Context) {
c.String(200, "pass")
})
go engine.Run(":18080")
defer func() {
engine.Server().Shutdown(context.TODO())
}()
time.Sleep(time.Millisecond * 20)
code, content, err := httpGet("http://127.0.0.1:18080/limit/test?appkey=testApp")
if err != nil {
t.Logf("http get failed,err:=%v", err)
t.FailNow()
}
if code != 200 || string(content) != "pass" {
t.Logf("request should pass by limiter,but blocked")
t.FailNow()
}
_, content, err = httpGet("http://127.0.0.1:18080/limit/test?appkey=testApp")
if err != nil {
t.Logf("http get failed,err:=%v", err)
t.FailNow()
}
if string(content) == "pass" {
t.Logf("request should block by limiter,but passed")
t.FailNow()
}
}
func httpGet(url string) (code int, content []byte, err error) {
resp, err := http.Get(url)
if err != nil {
return
}
defer resp.Body.Close()
content, err = ioutil.ReadAll(resp.Body)
if err != nil {
return
}
code = resp.StatusCode
return
}

View File

@@ -0,0 +1,51 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["supervisor_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
)
go_library(
name = "go_default_library",
srcs = ["supervisor.go"],
importpath = "go-common/library/net/http/blademaster/middleware/supervisor",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/ecode:go_default_library",
"//library/net/http/blademaster:go_default_library",
],
)
go_test(
name = "go_default_xtest",
srcs = ["example_test.go"],
tags = ["automanaged"],
deps = [
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/supervisor:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,9 @@
### business/blademaster/supervisor
##### Version 1.0.1
1. 使用 ecode 来拒绝请求
##### Version 1.0.0
1. 完成基本功能与测试

View File

@@ -0,0 +1,5 @@
# Author
lintnaghui
# Reviewer
maojian

View File

@@ -0,0 +1,7 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- lintnaghui
reviewers:
- lintnaghui
- maojian

View File

@@ -0,0 +1,13 @@
#### business/blademaster/supervisor
##### 项目简介
blademaster 的 supervisor middleware主要用于配置路由的开放策略
##### 编译环境
- **请只用 Golang v1.8.x 以上版本编译执行**
##### 依赖包
- No other dependency

View File

@@ -0,0 +1,28 @@
package supervisor_test
import (
"go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/supervisor"
"time"
)
// This example create a supervisor middleware instance and attach to a blademaster engine,
// it will allow '/ping' API can be requested with specified policy.
// This example will block all http method except `GET` on '/ping' API in next hour,
// and allow in further.
func Example() {
now := time.Now()
end := now.Add(time.Hour * 1)
spv := supervisor.New(&supervisor.Config{
On: true,
Begin: now,
End: end,
})
engine := blademaster.Default()
engine.Use(spv)
engine.GET("/ping", func(c *blademaster.Context) {
c.String(200, "%s", "pong")
})
engine.Run(":18080")
}

View File

@@ -0,0 +1,56 @@
package supervisor
import (
"time"
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
)
// Config supervisor conf.
type Config struct {
On bool // all post/put/delete method off.
Begin time.Time // begin time
End time.Time // end time
}
// Supervisor supervisor midleware.
type Supervisor struct {
conf *Config
on bool
}
// New new and return supervisor midleware.
func New(c *Config) (s *Supervisor) {
s = &Supervisor{
conf: c,
}
s.Reload(c)
return
}
// Reload reload supervisor conf.
func (s *Supervisor) Reload(c *Config) {
if c == nil {
return
}
s.on = c.On && c.Begin.Before(c.End)
s.conf = c // NOTE datarace but no side effect.
}
func (s *Supervisor) ServeHTTP(c *bm.Context) {
if s.on {
now := time.Now()
method := c.Request.Method
if s.forbid(method, now) {
c.JSON(nil, ecode.ServiceUpdate)
c.Abort()
return
}
}
}
func (s *Supervisor) forbid(method string, now time.Time) bool {
// only allow GET request.
return method != "GET" && now.Before(s.conf.End) && now.After(s.conf.Begin)
}

View File

@@ -0,0 +1,55 @@
package supervisor
import (
"testing"
"time"
)
func create() *Supervisor {
now := time.Now()
end := now.Add(time.Hour * 1)
conf := &Config{
On: true,
Begin: now,
End: end,
}
return New(conf)
}
func TestSupervisor(t *testing.T) {
sv := create()
in := sv.conf.Begin.Add(time.Second * 10)
out := sv.conf.End.Add(time.Second * 10)
if sv.forbid("GET", in) {
t.Error("Request should never be blocked on GET method")
}
if !sv.forbid("POST", in) {
t.Errorf("Request should be blocked on POST method at %+v", in)
}
if sv.forbid("POST", out) {
t.Errorf("Request should not be blocked at %+v", out)
}
}
func TestReload(t *testing.T) {
zero := time.Unix(0, 0)
conf := &Config{
On: false,
Begin: zero,
End: zero,
}
sv := create()
// reload with nil
sv.Reload(nil)
// reload with valid config
sv.Reload(conf)
if sv.conf != conf && sv.on == false {
t.Errorf("Failed to reload config %+v, current config is %+v", conf, sv.conf)
}
}

View File

@@ -0,0 +1,45 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = ["tag.go"],
importpath = "go-common/library/net/http/blademaster/middleware/tag",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = ["//library/net/http/blademaster:go_default_library"],
)
go_test(
name = "go_default_xtest",
srcs = [
"example_test.go",
"tag_test.go",
],
tags = ["automanaged"],
deps = [
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/tag:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,5 @@
### business/blademaster/tag
##### Version 1.0.0
1. 完成基本功能与测试

View File

@@ -0,0 +1,9 @@
# Owner
maojian
# Author
zhuangzhewei
# Reviewer
zhoujiahui
maojian

View File

@@ -0,0 +1,9 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- maojian
- zhuangzhewei
reviewers:
- maojian
- zhoujiahui
- zhuangzhewei

View File

@@ -0,0 +1,13 @@
#### business/blademaster/tag
##### 项目简介
blademaster 的 tag middleware主要用于实现在上下文内创建标签
##### 编译环境
- **请只用 Golang v1.8.x 以上版本编译执行**
##### 依赖包
- No other dependency

View File

@@ -0,0 +1,56 @@
package tag_test
import (
"go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/tag"
)
// This example create a tag middleware instance and attach to a global.
// It will put a tag into Keys field of context by specified policy custom defines
// You can define a custom policy through any request param or http header.
// Register several tag middlewares to put several tags.
func Example() {
var pf tag.PolicyFunc
// create your tag policy
pf = func(ctx *blademaster.Context) string {
if ctx.Request.Form.Get("group") == "a" {
return "a"
}
return "b"
}
t := tag.New("abtest", pf)
engine := blademaster.Default()
engine.Use(t)
engine.GET("/abtest", HandlerMap)
engine.Run(":18080")
}
func HandlerMap(ctx *blademaster.Context) {
value, ok := tag.Value(ctx, "abtest")
if !ok {
ctx.String(-400, "failed to parse group")
ctx.Abort()
return
}
if value == "a" {
HandlerA(ctx)
}
if value == "b" {
HandlerB(ctx)
}
}
func HandlerA(ctx *blademaster.Context) {
// your business
ctx.String(200, "group a")
return
}
func HandlerB(ctx *blademaster.Context) {
// your business
ctx.String(200, "group b")
return
}

View File

@@ -0,0 +1,48 @@
package tag
import (
bm "go-common/library/net/http/blademaster"
)
// Tag create a tag into Keys field of context
type Tag struct {
Name string
policy Policy
}
// Policy is a tag policy defined by custom
type Policy interface {
Tag(ctx *bm.Context) string
}
// PolicyFunc is a policy function
type PolicyFunc func(ctx *bm.Context) string
// Tag calls p(ctx)
func (p PolicyFunc) Tag(ctx *bm.Context) string {
return p(ctx)
}
// New create a new tag
func New(name string, p Policy) (tag *Tag) {
if p == nil {
panic("policy can not be nil")
}
tag = new(Tag)
tag.Name = name
tag.policy = p
return
}
// ServeHTTP implements from Handler interface
func (t *Tag) ServeHTTP(ctx *bm.Context) {
ctx.Keys[t.Name] = t.policy.Tag(ctx)
}
// Value will return tag value from context by input tag name
func Value(ctx *bm.Context, name string) (value string, ok bool) {
value, ok = ctx.Keys[name].(string)
return
}

View File

@@ -0,0 +1,71 @@
package tag_test
import (
"context"
"io/ioutil"
"net/http"
"testing"
"time"
"go-common/library/log"
"go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/tag"
"github.com/stretchr/testify/assert"
)
func init() {
log.Init(nil)
}
func makeBlueGreenTag() *tag.Tag {
var pf tag.PolicyFunc
pf = func(ctx *blademaster.Context) string {
if ctx.Request.Form.Get("color") == "blue" {
return "blue"
}
return "green"
}
t := tag.New("BlueGreen", pf)
return t
}
func TestBlueGreen(t *testing.T) {
tg := makeBlueGreenTag()
engine := blademaster.Default()
engine.Use(tg)
engine.GET("/bgget", func(ctx *blademaster.Context) {
color, ok := tag.Value(ctx, "BlueGreen")
if !ok {
ctx.Abort()
return
}
ctx.String(200, "color is: "+color)
})
go func() {
engine.Run(":18080")
}()
defer func() {
engine.Server().Shutdown(context.TODO())
}()
time.Sleep(1 * time.Second)
client := new(http.Client)
resp, err := client.Get("http://127.0.0.1:18080/bgget?color=blue")
assert.Nil(t, err)
defer resp.Body.Close()
assert.Equal(t, resp.StatusCode, 200)
body, err := ioutil.ReadAll(resp.Body)
assert.Nil(t, err)
assert.Equal(t, string(body), "color is: blue")
resp, err = client.Get("http://127.0.0.1:18080/bgget?color=green")
assert.Nil(t, err)
assert.Equal(t, resp.StatusCode, 200)
body, err = ioutil.ReadAll(resp.Body)
assert.Nil(t, err)
assert.Equal(t, string(body), "color is: green")
}

View File

@@ -0,0 +1,64 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["verify_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/metadata:go_default_library",
"//library/net/netutil/breaker:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = ["verify.go"],
importpath = "go-common/library/net/http/blademaster/middleware/verify",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/metadata:go_default_library",
"//library/time:go_default_library",
],
)
go_test(
name = "go_default_xtest",
srcs = ["example_test.go"],
tags = ["automanaged"],
deps = [
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/verify:go_default_library",
"//library/net/metadata:go_default_library",
"//library/time:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,5 @@
#### library/net/http/blademaster/middleware/verify
##### Version 1.0.0
1. 独立的 bm verify 中间件

View File

@@ -0,0 +1,10 @@
# Owner
maojian
zhoujiahui
# Author
zhoujiahui
# Reviewer
maojian
haoguanwei

View File

@@ -0,0 +1,9 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- maojian
- zhoujiahui
reviewers:
- haoguanwei
- maojian
- zhoujiahui

View File

@@ -0,0 +1,13 @@
#### library/net/http/blademaster/middleware/verify
##### 项目简介
blademaster 的 verify middleware主要用于设置路由的校验策略
##### 编译环境
- **请只用 Golang v1.10.x 以上版本编译执行**
##### 依赖包
- No other dependency

View File

@@ -0,0 +1,41 @@
package verify_test
import (
"fmt"
"time"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/verify"
"go-common/library/net/metadata"
xtime "go-common/library/time"
)
// This example create a identify middleware instance and attach to several path,
// it will validate request by specified policy and put extra information into context. e.g., `mid`.
// It provides additional handler functions to provide the identification for your business handler.
func Example() {
idt := verify.New(&verify.Config{
OpenServiceHost: "http://uat-open.bilibili.co",
HTTPClient: &bm.ClientConfig{
App: &bm.App{
Key: "53e2fa226f5ad348",
Secret: "3cf6bd1b0ff671021da5f424fea4b04a",
},
Dial: xtime.Duration(time.Second),
Timeout: xtime.Duration(time.Second),
KeepAlive: xtime.Duration(time.Second * 10),
},
})
e := bm.Default()
// mark `/verify` path as Verify policy
e.GET("/verify", idt.Verify, func(c *bm.Context) {
c.JSON("pass", nil)
})
// mark `/verify` path as VerifyUser policy
e.GET("/verifyUser", idt.VerifyUser, func(c *bm.Context) {
mid := metadata.Int64(c, metadata.Mid)
c.JSON(fmt.Sprintf("%d", mid), nil)
})
e.Run(":18080")
}

View File

@@ -0,0 +1,172 @@
package verify
import (
"crypto/md5"
"encoding/hex"
"net/url"
"strings"
"sync"
"time"
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/metadata"
xtime "go-common/library/time"
)
const (
_secretURI = "/api/getsecret"
)
// Verify is is the verify model.
type Verify struct {
lock sync.RWMutex
keys map[string]string
secretURI string
client *bm.Client
}
// Config is the verify config model.
type Config struct {
OpenServiceHost string
HTTPClient *bm.ClientConfig
}
var _defaultConfig = &Config{
OpenServiceHost: "http://open.bilibili.co",
HTTPClient: &bm.ClientConfig{
App: &bm.App{
Key: "53e2fa226f5ad348",
Secret: "3cf6bd1b0ff671021da5f424fea4b04a",
},
Dial: xtime.Duration(time.Millisecond * 100),
Timeout: xtime.Duration(time.Millisecond * 300),
KeepAlive: xtime.Duration(time.Second * 60),
},
}
// New will create a verify middleware by given config.
// panic on conf is nil.
func New(conf *Config) *Verify {
if conf == nil {
conf = _defaultConfig
}
v := &Verify{
keys: make(map[string]string),
client: bm.NewClient(conf.HTTPClient),
secretURI: conf.OpenServiceHost + _secretURI,
}
return v
}
func (v *Verify) verify(ctx *bm.Context) error {
req := ctx.Request
params := req.Form
if req.Method == "POST" {
// Give priority to sign in url query, otherwise check sign in post form.
q := req.URL.Query()
if q.Get("sign") != "" {
params = q
}
}
// check timestamp is not empty (TODO : Check if out of some seconds.., like 100s)
if params.Get("ts") == "" {
log.Error("ts is empty")
return ecode.RequestErr
}
sign := params.Get("sign")
params.Del("sign")
defer params.Set("sign", sign)
sappkey := params.Get("appkey")
v.lock.RLock()
secret, ok := v.keys[sappkey]
v.lock.RUnlock()
if !ok {
fetched, err := v.fetchSecret(ctx, sappkey)
if err != nil {
return err
}
v.lock.Lock()
v.keys[sappkey] = fetched
v.lock.Unlock()
secret = fetched
}
if hsign := Sign(params, sappkey, secret, true); hsign != sign {
if hsign1 := Sign(params, sappkey, secret, false); hsign1 != sign {
log.Error("Get sign: %s, expect %s", sign, hsign)
return ecode.SignCheckErr
}
}
return nil
}
// Verify will inject into handler func as verify required
func (v *Verify) Verify(ctx *bm.Context) {
if err := v.verify(ctx); err != nil {
ctx.JSON(nil, err)
ctx.Abort()
return
}
}
// VerifyUser is used to mark path as verify and mid required.
func (v *Verify) VerifyUser(ctx *bm.Context) {
if err := v.verify(ctx); err != nil {
ctx.JSON(nil, err)
ctx.Abort()
return
}
var midReq struct {
Mid int64 `form:"mid" validate:"required"`
}
if err := ctx.Bind(&midReq); err != nil {
return
}
ctx.Set("mid", midReq.Mid)
if md, ok := metadata.FromContext(ctx); ok {
md[metadata.Mid] = midReq.Mid
}
}
// Sign is used to sign form params by given condition.
func Sign(params url.Values, appkey string, secret string, lower bool) string {
data := params.Encode()
if strings.IndexByte(data, '+') > -1 {
data = strings.Replace(data, "+", "%20", -1)
}
if lower {
data = strings.ToLower(data)
}
digest := md5.Sum([]byte(data + secret))
return hex.EncodeToString(digest[:])
}
func (v *Verify) fetchSecret(ctx *bm.Context, appkey string) (string, error) {
params := url.Values{}
var resp struct {
Code int `json:"code"`
Data struct {
AppSecret string `json:"app_secret"`
} `json:"data"`
}
params.Set("sappkey", appkey)
if err := v.client.Get(ctx, v.secretURI, metadata.String(ctx, metadata.RemoteIP), params, &resp); err != nil {
return "", err
}
if resp.Code != 0 || resp.Data.AppSecret == "" {
log.Error("Failed to fetch secret with request(%s, %s) code(%d)", v.secretURI, params.Encode(), resp.Code)
if resp.Code != 0 {
return "", ecode.Int(resp.Code)
}
return "", ecode.ServerErr
}
return resp.Data.AppSecret, nil
}

View File

@@ -0,0 +1,175 @@
package verify
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"testing"
"time"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/metadata"
"go-common/library/net/netutil/breaker"
xtime "go-common/library/time"
"github.com/stretchr/testify/assert"
)
type Response struct {
Code int `json:"code"`
Data string `json:"data"`
}
func init() {
log.Init(&log.Config{
Stdout: true,
})
}
func verify() *Verify {
return New(&Config{
OpenServiceHost: "http://uat-open.bilibili.co",
HTTPClient: &bm.ClientConfig{
App: &bm.App{
Key: "53e2fa226f5ad348",
Secret: "3cf6bd1b0ff671021da5f424fea4b04a",
},
Dial: xtime.Duration(time.Second),
Timeout: xtime.Duration(time.Second),
KeepAlive: xtime.Duration(time.Second * 10),
Breaker: &breaker.Config{
Window: xtime.Duration(time.Second),
Sleep: xtime.Duration(time.Millisecond * 100),
Bucket: 10,
Ratio: 0.5,
Request: 100,
},
},
})
}
func client() *bm.Client {
return bm.NewClient(&bm.ClientConfig{
App: &bm.App{
Key: "53e2fa226f5ad348",
Secret: "3cf6bd1b0ff671021da5f424fea4b04a",
},
Dial: xtime.Duration(time.Second),
Timeout: xtime.Duration(time.Second),
KeepAlive: xtime.Duration(time.Second * 10),
Breaker: &breaker.Config{
Window: xtime.Duration(time.Second),
Sleep: xtime.Duration(time.Millisecond * 100),
Bucket: 10,
Ratio: 0.5,
Request: 100,
},
})
}
func engine() *bm.Engine {
e := bm.New()
idt := verify()
e.GET("/verify", idt.Verify, func(c *bm.Context) {
c.JSON("pass", nil)
})
e.GET("/verifyUser", idt.VerifyUser, func(c *bm.Context) {
mid := metadata.Int64(c, metadata.Mid)
fmt.Println(mid)
c.JSON(fmt.Sprintf("%d", mid), nil)
})
return e
}
func TestNewWithNilConfig(t *testing.T) {
New(nil)
}
func TestVerifyIdentifyHandler(t *testing.T) {
e := engine()
go e.Run(":18080")
time.Sleep(time.Second)
// test cases
testVerifyFailed(t)
testVerifySuccess(t)
testVerifyUser(t)
testVerifyUserFailed(t)
testVerifyUserInvalid(t)
if err := e.Server().Shutdown(context.TODO()); err != nil {
t.Logf("Failed to shutdown bm engine: %v", err)
}
}
func httpGet(url string) (code int, content []byte, err error) {
resp, err := http.Get(url)
if err != nil {
return
}
defer resp.Body.Close()
content, err = ioutil.ReadAll(resp.Body)
if err != nil {
return
}
code = resp.StatusCode
return
}
func testVerifyFailed(t *testing.T) {
res := Response{}
code, content, err := httpGet("http://127.0.0.1:18080/verify?ts=1&appkey=53e2fa226f5ad348")
assert.NoError(t, err)
assert.Equal(t, 200, code)
err = json.Unmarshal(content, &res)
assert.NoError(t, err)
assert.Equal(t, -3, res.Code)
}
func testVerifySuccess(t *testing.T) {
res := Response{}
uv := url.Values{}
uv.Set("appkey", "53e2fa226f5ad348")
err := client().Get(context.TODO(), "http://127.0.0.1:18080/verify", "", uv, &res)
assert.NoError(t, err)
assert.Equal(t, 0, res.Code)
assert.Equal(t, "pass", res.Data)
}
func testVerifyUser(t *testing.T) {
res := Response{}
query := url.Values{}
query.Set("mid", "1")
query.Set("appkey", "53e2fa226f5ad348")
err := client().Get(context.TODO(), "http://127.0.0.1:18080/verifyUser", "", query, &res)
assert.NoError(t, err)
assert.Equal(t, 0, res.Code)
assert.Equal(t, "1", res.Data)
}
func testVerifyUserFailed(t *testing.T) {
res := Response{}
code, content, err := httpGet("http://127.0.0.1:18080/verifyUser?ts=1&appkey=53e2fa226f5ad348")
assert.NoError(t, err)
assert.Equal(t, 200, code)
err = json.Unmarshal(content, &res)
assert.NoError(t, err)
assert.Equal(t, -3, res.Code)
}
func testVerifyUserInvalid(t *testing.T) {
res := Response{}
query := url.Values{}
query.Set("mid", "aaaa/")
query.Set("appkey", "53e2fa226f5ad348")
err := client().Get(context.TODO(), "http://127.0.0.1:18080/verifyUser", "", query, &res)
assert.NoError(t, err)
assert.Equal(t, -400, res.Code)
assert.Equal(t, "", res.Data)
}