From 90827b24411651b52264369a5218743ab62d7f89 Mon Sep 17 00:00:00 2001 From: HFO4 <912394456@qq.com> Date: Mon, 23 Dec 2019 13:27:18 +0800 Subject: [PATCH] Feat: sign http request / read running mode from config file --- conf/conf.ini | 3 ++ go.mod | 1 + main.go | 11 ++--- pkg/auth/auth.go | 37 +++++++++++++++- pkg/auth/auth_test.go | 24 ++++++++++ pkg/conf/conf.go | 5 ++- pkg/conf/defaults.go | 4 +- pkg/serializer/auth.go | 22 +++++++++ pkg/serializer/auth_test.go | 13 ++++++ routers/controllers/slave.go | 14 ++++++ routers/file_router_test.go | 6 +-- routers/router.go | 86 ++++++++++++++++++++++++++---------- routers/router_test.go | 10 ++--- 13 files changed, 195 insertions(+), 41 deletions(-) create mode 100644 pkg/serializer/auth.go create mode 100644 pkg/serializer/auth_test.go create mode 100644 routers/controllers/slave.go diff --git a/conf/conf.ini b/conf/conf.ini index 25ff95b..ecdbf97 100644 --- a/conf/conf.ini +++ b/conf/conf.ini @@ -1,6 +1,9 @@ [System] +Mode = slave +Listen = :5000 Debug = true SessionSecret = 23333 +SlaveSecret = 1234567891234567123456789123456712345678912345671234567891234567 [Thumbnail] MaxWidth = 400 diff --git a/go.mod b/go.mod index a7c183e..2b01584 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.12 require ( github.com/DATA-DOG/go-sqlmock v1.3.3 + github.com/cloudflare/cfssl v0.0.0-20190726000631-633726f6bcb7 github.com/duo-labs/webauthn v0.0.0-20191119193225-4bf9a0f776d4 github.com/fatih/color v1.7.0 github.com/garyburd/redigo v1.6.0 diff --git a/main.go b/main.go index 3f039e4..9cdc9c0 100644 --- a/main.go +++ b/main.go @@ -12,20 +12,21 @@ import ( func init() { conf.Init("conf/conf.ini") - cache.Init() - model.Init() - // Debug 关闭时,切换为生产模式 if !conf.SystemConfig.Debug { gin.SetMode(gin.ReleaseMode) } + cache.Init() + if conf.SystemConfig.Mode == "master" { + model.Init() + authn.Init() + } auth.Init() - authn.Init() } func main() { api := routers.InitRouter() - api.Run(":5000") + api.Run(conf.SystemConfig.Listen) } diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 2c9cc75..5e05922 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -2,7 +2,11 @@ package auth import ( model "github.com/HFO4/cloudreve/models" + "github.com/HFO4/cloudreve/pkg/conf" "github.com/HFO4/cloudreve/pkg/serializer" + "github.com/HFO4/cloudreve/pkg/util" + "io/ioutil" + "net/http" "net/url" ) @@ -22,6 +26,26 @@ type Auth interface { Check(body string, sign string) error } +// SignRequest 对PUT\POST等复杂HTTP请求签名,如果请求Header中 +// 包含 X-Policy, 则此请求会被认定为上传请求,只会对URI部分和 +// Policy部分进行签名。其他请求则会对URI和Body部分进行签名。 +func SignRequest(r *http.Request, expires int64) *http.Request { + var rawSignString string + if policy, ok := r.Header["X-Policy"]; ok { + rawSignString = serializer.NewRequestSignString(r.URL.Path, policy[0], "") + } else { + body, _ := ioutil.ReadAll(r.Body) + rawSignString = serializer.NewRequestSignString(r.URL.Path, "", string(body)) + } + + // 生成签名 + sign := General.Sign(rawSignString, expires) + + // 将签名加到请求Header中 + r.Header["Authorization"] = []string{"Bearer " + sign} + return r +} + // SignURI 对URI进行签名,签名只针对Path部分,query部分不做验证 func SignURI(uri string, expires int64) (*url.URL, error) { base, err := url.Parse(uri) @@ -52,9 +76,18 @@ func CheckURI(url *url.URL) error { } // Init 初始化通用鉴权器 -// TODO slave模式下从配置文件获取 +// TODO 测试 func Init() { + var secretKey string + if conf.SystemConfig.Mode == "master" { + secretKey = model.GetSettingByName("secret_key") + } else { + secretKey = conf.SystemConfig.SlaveSecret + if secretKey == "" { + util.Log().Panic("未指定 SlaveSecret,请前往配置文件中指定") + } + } General = HMACAuth{ - SecretKey: []byte(model.GetSettingByName("secret_key")), + SecretKey: []byte(secretKey), } } diff --git a/pkg/auth/auth_test.go b/pkg/auth/auth_test.go index 9be8eb6..82dcacd 100644 --- a/pkg/auth/auth_test.go +++ b/pkg/auth/auth_test.go @@ -3,6 +3,8 @@ package auth import ( "github.com/HFO4/cloudreve/pkg/util" "github.com/stretchr/testify/assert" + "net/http" + "strings" "testing" "time" ) @@ -46,3 +48,25 @@ func TestCheckURI(t *testing.T) { asserts.Error(CheckURI(sign)) } } + +func TestSignRequest(t *testing.T) { + asserts := assert.New(t) + General = HMACAuth{SecretKey: []byte(util.RandStringRunes(256))} + + // 非上传请求 + { + req, err := http.NewRequest("POST", "http://127.0.0.1/api/v3/upload", strings.NewReader("I am body.")) + asserts.NoError(err) + req = SignRequest(req, 10) + asserts.NotEmpty(req.Header["Authorization"]) + } + + // 上传请求 + { + req, err := http.NewRequest("POST", "http://127.0.0.1/api/v3/upload", strings.NewReader("I am body.")) + asserts.NoError(err) + req.Header["X-Policy"] = []string{"I am Policy"} + req = SignRequest(req, 10) + asserts.NotEmpty(req.Header["Authorization"]) + } +} diff --git a/pkg/conf/conf.go b/pkg/conf/conf.go index 07484f6..29f9960 100644 --- a/pkg/conf/conf.go +++ b/pkg/conf/conf.go @@ -18,8 +18,11 @@ type database struct { // system 系统通用配置 type system struct { + Mode string `validate:"eq=master|eq=slave"` + Listen string `validate:"required"` Debug bool SessionSecret string + SlaveSecret string `validate:"omitempty,gte=64"` } // captcha 验证码配置 @@ -84,7 +87,7 @@ func Init(path string) { for sectionName, sectionStruct := range sections { err = mapSection(sectionName, sectionStruct) if err != nil { - util.Log().Warning("配置文件 %s 分区解析失败: %s", sectionName, err) + util.Log().Panic("配置文件 %s 分区解析失败: %s", sectionName, err) } } diff --git a/pkg/conf/defaults.go b/pkg/conf/defaults.go index 771fea5..f3e73de 100644 --- a/pkg/conf/defaults.go +++ b/pkg/conf/defaults.go @@ -16,7 +16,9 @@ var DatabaseConfig = &database{ // SystemConfig 系统公用配置 var SystemConfig = &system{ - Debug: false, + Debug: false, + Mode: "master", + Listen: ":5000", } // CaptchaConfig 验证码配置 diff --git a/pkg/serializer/auth.go b/pkg/serializer/auth.go new file mode 100644 index 0000000..7d11dff --- /dev/null +++ b/pkg/serializer/auth.go @@ -0,0 +1,22 @@ +package serializer + +import "encoding/json" + +// RequestRawSign 待签名的HTTP请求 +type RequestRawSign struct { + Path string + Policy string + Body string +} + +// NewRequestSignString 返回JSON格式的待签名字符串 +// TODO 测试 +func NewRequestSignString(path, policy, body string) string { + req := RequestRawSign{ + Path: path, + Policy: policy, + Body: body, + } + res, _ := json.Marshal(req) + return string(res) +} diff --git a/pkg/serializer/auth_test.go b/pkg/serializer/auth_test.go new file mode 100644 index 0000000..96b6b9b --- /dev/null +++ b/pkg/serializer/auth_test.go @@ -0,0 +1,13 @@ +package serializer + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNewRequestSignString(t *testing.T) { + asserts := assert.New(t) + + sign := NewRequestSignString("1", "2", "3") + asserts.NotEmpty(sign) +} diff --git a/routers/controllers/slave.go b/routers/controllers/slave.go new file mode 100644 index 0000000..524a686 --- /dev/null +++ b/routers/controllers/slave.go @@ -0,0 +1,14 @@ +package controllers + +import ( + "github.com/HFO4/cloudreve/pkg/serializer" + "github.com/gin-gonic/gin" +) + +// SlaveUpload 从机文件上传 +func SlaveUpload(c *gin.Context) { + + c.JSON(200, serializer.Response{ + Code: 0, + }) +} diff --git a/routers/file_router_test.go b/routers/file_router_test.go index 9a99e85..ea90b23 100644 --- a/routers/file_router_test.go +++ b/routers/file_router_test.go @@ -17,7 +17,7 @@ import ( func TestListDirectoryRoute(t *testing.T) { switchToMemDB() asserts := assert.New(t) - router := InitRouter() + router := InitMasterRouter() w := httptest.NewRecorder() // 成功 @@ -41,7 +41,7 @@ func TestListDirectoryRoute(t *testing.T) { func TestLocalFileUpload(t *testing.T) { switchToMemDB() asserts := assert.New(t) - router := InitRouter() + router := InitMasterRouter() w := httptest.NewRecorder() middleware.SessionMock = map[string]interface{}{"user_id": 1} @@ -111,7 +111,7 @@ func TestLocalFileUpload(t *testing.T) { func TestObjectDelete(t *testing.T) { asserts := assert.New(t) - router := InitRouter() + router := InitMasterRouter() w := httptest.NewRecorder() middleware.SessionMock = map[string]interface{}{"user_id": 1} diff --git a/routers/router.go b/routers/router.go index abb7355..58dce5a 100644 --- a/routers/router.go +++ b/routers/router.go @@ -3,42 +3,45 @@ package routers import ( "github.com/HFO4/cloudreve/middleware" "github.com/HFO4/cloudreve/pkg/conf" + "github.com/HFO4/cloudreve/pkg/util" "github.com/HFO4/cloudreve/routers/controllers" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) -// initWebDAV 初始化WebDAV相关路由 -func initWebDAV(group *gin.RouterGroup) { - { - group.Use(middleware.WebDAVAuth()) - - group.Any("/*path", controllers.ServeWebDAV) - group.Any("", controllers.ServeWebDAV) - group.Handle("PROPFIND", "/*path", controllers.ServeWebDAV) - group.Handle("PROPFIND", "", controllers.ServeWebDAV) - group.Handle("MKCOL", "/*path", controllers.ServeWebDAV) - group.Handle("LOCK", "/*path", controllers.ServeWebDAV) - group.Handle("UNLOCK", "/*path", controllers.ServeWebDAV) - group.Handle("PROPPATCH", "/*path", controllers.ServeWebDAV) - group.Handle("COPY", "/*path", controllers.ServeWebDAV) - group.Handle("MOVE", "/*path", controllers.ServeWebDAV) - +// InitRouter 初始化路由 +func InitRouter() *gin.Engine { + if conf.SystemConfig.Mode == "master" { + util.Log().Info("当前运行模式:Master") + return InitMasterRouter() } + util.Log().Info("当前运行模式:Slave") + return InitSlaveRouter() + } -// InitRouter 初始化路由 -func InitRouter() *gin.Engine { +// InitSlaveRouter 初始化从机模式路由 +func InitSlaveRouter() *gin.Engine { r := gin.Default() - v3 := r.Group("/api/v3") + v3 := r.Group("/api/v3/slave") + // 跨域相关 + InitCORS(v3) + // 鉴权中间件 + v3.Use(middleware.SignRequired()) + /* - 中间件 + 路由 */ - v3.Use(middleware.Session(conf.SystemConfig.SessionSecret)) + { + v3.POST("upload", controllers.SlaveUpload) + } + return r +} - // CORS TODO: 根据配置文件来 +// InitCORS 初始化跨域配置 +func InitCORS(group *gin.RouterGroup) { if conf.CORSConfig.AllowOrigins[0] != "UNSET" || conf.CORSConfig.AllowAllOrigins { - v3.Use(cors.New(cors.Config{ + group.Use(cors.New(cors.Config{ AllowOrigins: conf.CORSConfig.AllowOrigins, AllowAllOrigins: conf.CORSConfig.AllowAllOrigins, AllowMethods: conf.CORSConfig.AllowHeaders, @@ -46,13 +49,29 @@ func InitRouter() *gin.Engine { AllowCredentials: conf.CORSConfig.AllowCredentials, ExposeHeaders: conf.CORSConfig.ExposeHeaders, })) + return } + // slave模式下未启动跨域的警告 + if conf.SystemConfig.Mode == "slave" { + util.Log().Warning("当前作为存储端(Slave)运行,但未启用跨域配置,可能会导致 Master 端无法正常上传文件") + } +} + +// InitMasterRouter 初始化主机模式路由 +func InitMasterRouter() *gin.Engine { + r := gin.Default() + v3 := r.Group("/api/v3") + /* + 中间件 + */ + v3.Use(middleware.Session(conf.SystemConfig.SessionSecret)) + // 跨域相关 + InitCORS(v3) // 测试模式加入Mock助手中间件 if gin.Mode() == gin.TestMode { v3.Use(middleware.MockHelper()) } - v3.Use(middleware.CurrentUser()) /* @@ -166,3 +185,22 @@ func InitRouter() *gin.Engine { initWebDAV(r.Group("dav")) return r } + +// initWebDAV 初始化WebDAV相关路由 +func initWebDAV(group *gin.RouterGroup) { + { + group.Use(middleware.WebDAVAuth()) + + group.Any("/*path", controllers.ServeWebDAV) + group.Any("", controllers.ServeWebDAV) + group.Handle("PROPFIND", "/*path", controllers.ServeWebDAV) + group.Handle("PROPFIND", "", controllers.ServeWebDAV) + group.Handle("MKCOL", "/*path", controllers.ServeWebDAV) + group.Handle("LOCK", "/*path", controllers.ServeWebDAV) + group.Handle("UNLOCK", "/*path", controllers.ServeWebDAV) + group.Handle("PROPPATCH", "/*path", controllers.ServeWebDAV) + group.Handle("COPY", "/*path", controllers.ServeWebDAV) + group.Handle("MOVE", "/*path", controllers.ServeWebDAV) + + } +} diff --git a/routers/router_test.go b/routers/router_test.go index 0b0c56c..28b3746 100644 --- a/routers/router_test.go +++ b/routers/router_test.go @@ -11,7 +11,7 @@ import ( func TestPing(t *testing.T) { asserts := assert.New(t) - router := InitRouter() + router := InitMasterRouter() w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v3/site/ping", nil) @@ -23,7 +23,7 @@ func TestPing(t *testing.T) { func TestCaptcha(t *testing.T) { asserts := assert.New(t) - router := InitRouter() + router := InitMasterRouter() w := httptest.NewRecorder() req, _ := http.NewRequest( @@ -43,7 +43,7 @@ func TestCaptcha(t *testing.T) { // defer mutex.Unlock() // switchToMockDB() // asserts := assert.New(t) -// router := InitRouter() +// router := InitMasterRouter() // w := httptest.NewRecorder() // // // 创建测试用验证码 @@ -153,7 +153,7 @@ func TestCaptcha(t *testing.T) { // defer mutex.Unlock() // switchToMockDB() // asserts := assert.New(t) -// router := InitRouter() +// router := InitMasterRouter() // w := httptest.NewRecorder() // // mock.ExpectQuery("^SELECT (.+)").WillReturnRows(sqlmock.NewRows([]string{"email", "nick", "password", "options"}). @@ -211,7 +211,7 @@ func TestCaptcha(t *testing.T) { func TestSiteConfigRoute(t *testing.T) { switchToMemDB() asserts := assert.New(t) - router := InitRouter() + router := InitMasterRouter() w := httptest.NewRecorder() req, _ := http.NewRequest(