Compare commits

...
15 Commits
21 changed files with 508 additions and 172 deletions
-27
View File
@@ -5,35 +5,8 @@ on:
branches: [ master ]
jobs:
test:
name: Test
runs-on: ubuntu-16.04
steps:
- name: Set up Go 1.13
uses: actions/setup-go@v1
with:
go-version: 1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
with:
submodules: 'recursive'
- name: Get dependencies
run: |
go get github.com/rakyll/statik
export PATH=$PATH:~/go/bin/
statik -src=models -f
- name: Test
run: go test -coverprofile=coverage.txt -covermode=atomic ./...
build:
name: Build
needs: test
runs-on: ubuntu-16.04
steps:
+47
View File
@@ -0,0 +1,47 @@
name: Test
on:
pull_request:
branches:
- master
push:
branches: [ master ]
jobs:
test:
name: Test
runs-on: ubuntu-16.04
steps:
- name: Set up Go 1.13
uses: actions/setup-go@v1
with:
go-version: 1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
with:
submodules: 'recursive'
- name: Get dependencies
run: |
go get github.com/rakyll/statik
export PATH=$PATH:~/go/bin/
statik -src=models -f
- name: Test
run: go test -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload binary files (linux_arm)
uses: actions/upload-artifact@v2
with:
name: cloudreve_linux_arm
path: release/cloudreve*linux_arm.*
- name: Upload binary files (linux_arm64)
uses: actions/upload-artifact@v2
with:
name: cloudreve_linux_arm64
path: release/cloudreve*linux_arm64.*
-1
View File
@@ -27,4 +27,3 @@ version.lock
*.ini
conf/conf.ini
/statik/
/vendor/
+1 -1
Submodule assets updated: 35c5966f66...522a75c750
+122
View File
@@ -0,0 +1,122 @@
package middleware
import (
"bytes"
"encoding/json"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/recaptcha"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha"
captcha "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/captcha/v20190722"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
"io"
"io/ioutil"
"strconv"
"time"
)
type req struct {
CaptchaCode string `json:"captchaCode"`
Ticket string `json:"ticket"`
Randstr string `json:"randstr"`
}
// CaptchaRequired 验证请求签名
func CaptchaRequired(configName string) gin.HandlerFunc {
return func(c *gin.Context) {
// 相关设定
options := model.GetSettingByNames(configName,
"captcha_type",
"captcha_ReCaptchaSecret",
"captcha_TCaptcha_SecretId",
"captcha_TCaptcha_SecretKey",
"captcha_TCaptcha_CaptchaAppId",
"captcha_TCaptcha_AppSecretKey")
// 检查验证码
isCaptchaRequired := model.IsTrueVal(options[configName])
if isCaptchaRequired {
var service req
bodyCopy := new(bytes.Buffer)
_, err := io.Copy(bodyCopy, c.Request.Body)
if err != nil {
c.JSON(200, serializer.ParamErr("验证码错误", err))
c.Abort()
return
}
bodyData := bodyCopy.Bytes()
err = json.Unmarshal(bodyData, &service)
if err != nil {
c.JSON(200, serializer.ParamErr("验证码错误", err))
c.Abort()
return
}
c.Request.Body = ioutil.NopCloser(bytes.NewReader(bodyData))
switch options["captcha_type"] {
case "normal":
captchaID := util.GetSession(c, "captchaID")
util.DeleteSession(c, "captchaID")
if captchaID == nil || !base64Captcha.VerifyCaptcha(captchaID.(string), service.CaptchaCode) {
c.JSON(200, serializer.ParamErr("验证码错误", nil))
c.Abort()
return
}
break
case "recaptcha":
reCAPTCHA, err := recaptcha.NewReCAPTCHA(options["captcha_ReCaptchaSecret"], recaptcha.V2, 10*time.Second)
if err != nil {
util.Log().Warning("reCAPTCHA 验证错误, %s", err)
c.Abort()
break
}
err = reCAPTCHA.Verify(service.CaptchaCode)
if err != nil {
util.Log().Warning("reCAPTCHA 验证错误, %s", err)
c.JSON(200, serializer.ParamErr("验证失败,请刷新网页后再次验证", nil))
c.Abort()
return
}
break
case "tcaptcha":
credential := common.NewCredential(
options["captcha_TCaptcha_SecretId"],
options["captcha_TCaptcha_SecretKey"],
)
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "captcha.tencentcloudapi.com"
client, _ := captcha.NewClient(credential, "", cpf)
request := captcha.NewDescribeCaptchaResultRequest()
request.CaptchaType = common.Uint64Ptr(9)
appid, _ := strconv.Atoi(options["captcha_TCaptcha_CaptchaAppId"])
request.CaptchaAppId = common.Uint64Ptr(uint64(appid))
request.AppSecretKey = common.StringPtr(options["captcha_TCaptcha_AppSecretKey"])
request.Ticket = common.StringPtr(service.Ticket)
request.Randstr = common.StringPtr(service.Randstr)
request.UserIp = common.StringPtr(c.ClientIP())
response, err := client.DescribeCaptchaResult(request)
if err != nil {
util.Log().Warning("TCaptcha 验证错误, %s", err)
c.Abort()
break
}
if *response.Response.CaptchaCode != int64(1) {
c.JSON(200, serializer.ParamErr("验证失败,请刷新网页后再次验证", nil))
c.Abort()
return
}
break
}
}
c.Next()
}
}
+177
View File
@@ -0,0 +1,177 @@
package middleware
import (
"bytes"
"errors"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"
)
type errReader int
func (errReader) Read(p []byte) (n int, err error) {
return 0, errors.New("test error")
}
func TestCaptchaRequired_General(t *testing.T) {
asserts := assert.New(t)
rec := httptest.NewRecorder()
// 未启用验证码
{
cache.SetSettings(map[string]string{
"login_captcha": "0",
"captcha_type": "1",
"captcha_ReCaptchaSecret": "1",
"captcha_TCaptcha_SecretId": "1",
"captcha_TCaptcha_SecretKey": "1",
"captcha_TCaptcha_CaptchaAppId": "1",
"captcha_TCaptcha_AppSecretKey": "1",
}, "setting_")
TestFunc := CaptchaRequired("login_captcha")
c, _ := gin.CreateTestContext(rec)
c.Params = []gin.Param{}
c.Request, _ = http.NewRequest("GET", "/", nil)
TestFunc(c)
asserts.False(c.IsAborted())
}
// body 无法读取
{
cache.SetSettings(map[string]string{
"login_captcha": "1",
"captcha_type": "1",
"captcha_ReCaptchaSecret": "1",
"captcha_TCaptcha_SecretId": "1",
"captcha_TCaptcha_SecretKey": "1",
"captcha_TCaptcha_CaptchaAppId": "1",
"captcha_TCaptcha_AppSecretKey": "1",
}, "setting_")
TestFunc := CaptchaRequired("login_captcha")
c, _ := gin.CreateTestContext(rec)
c.Params = []gin.Param{}
c.Request, _ = http.NewRequest("GET", "/", errReader(1))
TestFunc(c)
asserts.True(c.IsAborted())
}
// body JSON 解析失败
{
cache.SetSettings(map[string]string{
"login_captcha": "1",
"captcha_type": "1",
"captcha_ReCaptchaSecret": "1",
"captcha_TCaptcha_SecretId": "1",
"captcha_TCaptcha_SecretKey": "1",
"captcha_TCaptcha_CaptchaAppId": "1",
"captcha_TCaptcha_AppSecretKey": "1",
}, "setting_")
TestFunc := CaptchaRequired("login_captcha")
c, _ := gin.CreateTestContext(rec)
c.Params = []gin.Param{}
r := bytes.NewReader([]byte("123"))
c.Request, _ = http.NewRequest("GET", "/", r)
TestFunc(c)
asserts.True(c.IsAborted())
}
}
func TestCaptchaRequired_Normal(t *testing.T) {
asserts := assert.New(t)
rec := httptest.NewRecorder()
// 验证码错误
{
cache.SetSettings(map[string]string{
"login_captcha": "1",
"captcha_type": "normal",
"captcha_ReCaptchaSecret": "1",
"captcha_TCaptcha_SecretId": "1",
"captcha_TCaptcha_SecretKey": "1",
"captcha_TCaptcha_CaptchaAppId": "1",
"captcha_TCaptcha_AppSecretKey": "1",
}, "setting_")
TestFunc := CaptchaRequired("login_captcha")
c, _ := gin.CreateTestContext(rec)
c.Params = []gin.Param{}
r := bytes.NewReader([]byte("{}"))
c.Request, _ = http.NewRequest("GET", "/", r)
Session("233")(c)
TestFunc(c)
asserts.True(c.IsAborted())
}
}
func TestCaptchaRequired_Recaptcha(t *testing.T) {
asserts := assert.New(t)
rec := httptest.NewRecorder()
// 无法初始化reCaptcha实例
{
cache.SetSettings(map[string]string{
"login_captcha": "1",
"captcha_type": "recaptcha",
"captcha_ReCaptchaSecret": "",
"captcha_TCaptcha_SecretId": "1",
"captcha_TCaptcha_SecretKey": "1",
"captcha_TCaptcha_CaptchaAppId": "1",
"captcha_TCaptcha_AppSecretKey": "1",
}, "setting_")
TestFunc := CaptchaRequired("login_captcha")
c, _ := gin.CreateTestContext(rec)
c.Params = []gin.Param{}
r := bytes.NewReader([]byte("{}"))
c.Request, _ = http.NewRequest("GET", "/", r)
TestFunc(c)
asserts.True(c.IsAborted())
}
// 验证码错误
{
cache.SetSettings(map[string]string{
"login_captcha": "1",
"captcha_type": "recaptcha",
"captcha_ReCaptchaSecret": "233",
"captcha_TCaptcha_SecretId": "1",
"captcha_TCaptcha_SecretKey": "1",
"captcha_TCaptcha_CaptchaAppId": "1",
"captcha_TCaptcha_AppSecretKey": "1",
}, "setting_")
TestFunc := CaptchaRequired("login_captcha")
c, _ := gin.CreateTestContext(rec)
c.Params = []gin.Param{}
r := bytes.NewReader([]byte("{}"))
c.Request, _ = http.NewRequest("GET", "/", r)
TestFunc(c)
asserts.True(c.IsAborted())
}
}
func TestCaptchaRequired_Tcaptcha(t *testing.T) {
asserts := assert.New(t)
rec := httptest.NewRecorder()
// 验证出错
{
cache.SetSettings(map[string]string{
"login_captcha": "1",
"captcha_type": "tcaptcha",
"captcha_ReCaptchaSecret": "",
"captcha_TCaptcha_SecretId": "1",
"captcha_TCaptcha_SecretKey": "1",
"captcha_TCaptcha_CaptchaAppId": "1",
"captcha_TCaptcha_AppSecretKey": "1",
}, "setting_")
TestFunc := CaptchaRequired("login_captcha")
c, _ := gin.CreateTestContext(rec)
c.Params = []gin.Param{}
r := bytes.NewReader([]byte("{}"))
c.Request, _ = http.NewRequest("GET", "/", r)
TestFunc(c)
asserts.True(c.IsAborted())
}
}
+5 -1
View File
@@ -149,6 +149,7 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti
{Name: "share_view_method", Value: "list", Type: "view"},
{Name: "cron_garbage_collect", Value: "@hourly", Type: "cron"},
{Name: "authn_enabled", Value: "0", Type: "authn"},
{Name: "captcha_type", Value: "normal", Type: "captcha"},
{Name: "captcha_height", Value: "60", Type: "captcha"},
{Name: "captcha_width", Value: "240", Type: "captcha"},
{Name: "captcha_mode", Value: "3", Type: "captcha"},
@@ -160,9 +161,12 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti
{Name: "captcha_IsShowSlimeLine", Value: "1", Type: "captcha"},
{Name: "captcha_IsShowSineLine", Value: "0", Type: "captcha"},
{Name: "captcha_CaptchaLen", Value: "6", Type: "captcha"},
{Name: "captcha_IsUseReCaptcha", Value: "0", Type: "captcha"},
{Name: "captcha_ReCaptchaKey", Value: "defaultKey", Type: "captcha"},
{Name: "captcha_ReCaptchaSecret", Value: "defaultSecret", Type: "captcha"},
{Name: "captcha_TCaptcha_CaptchaAppId", Value: "", Type: "captcha"},
{Name: "captcha_TCaptcha_AppSecretKey", Value: "", Type: "captcha"},
{Name: "captcha_TCaptcha_SecretId", Value: "", Type: "captcha"},
{Name: "captcha_TCaptcha_SecretKey", Value: "", Type: "captcha"},
{Name: "thumb_width", Value: "400", Type: "thumb"},
{Name: "thumb_height", Value: "300", Type: "thumb"},
{Name: "pwa_small_icon", Value: "/static/img/favicon.ico", Type: "pwa"},
+3 -3
View File
@@ -1,13 +1,13 @@
package conf
// BackendVersion 当前后端版本号
var BackendVersion = "3.3.0"
var BackendVersion = "3.3.1"
// RequiredDBVersion 与当前版本匹配的数据库版本
var RequiredDBVersion = "3.3.0"
var RequiredDBVersion = "3.3.1"
// RequiredStaticVersion 与当前版本匹配的静态资源版本
var RequiredStaticVersion = "3.3.0"
var RequiredStaticVersion = "3.3.1"
// IsPro 是否为Pro版本
var IsPro = "false"
+14
View File
@@ -12,6 +12,7 @@ import (
"time"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
@@ -155,6 +156,19 @@ func (handler Driver) Source(
cacheKey := fmt.Sprintf("onedrive_source_%d_%s", handler.Policy.ID, path)
if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok {
cacheKey = fmt.Sprintf("onedrive_source_file_%d_%d", file.UpdatedAt.Unix(), file.ID)
// 如果是永久链接,则返回签名后的中转外链
if ttl == 0 {
signedURI, err := auth.SignURI(
auth.General,
fmt.Sprintf("/api/v3/file/source/%d/%s", file.ID, file.Name),
ttl,
)
if err != nil {
return "", err
}
return baseURL.ResolveReference(signedURI).String(), nil
}
}
// 尝试从缓存中查找
+20 -4
View File
@@ -3,6 +3,7 @@ package onedrive
import (
"context"
"fmt"
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
"io"
"io/ioutil"
"net/http"
@@ -135,7 +136,7 @@ func TestDriver_Source(t *testing.T) {
// 失败
{
res, err := handler.Source(context.Background(), "123.jpg", url.URL{}, 0, true, 0)
res, err := handler.Source(context.Background(), "123.jpg", url.URL{}, 1, true, 0)
asserts.Error(err)
asserts.Empty(res)
}
@@ -144,7 +145,7 @@ func TestDriver_Source(t *testing.T) {
{
handler.Client.Credential.ExpiresIn = time.Now().Add(time.Duration(100) * time.Hour).Unix()
handler.Client.Credential.AccessToken = "1"
cache.Set("onedrive_source_0_123.jpg", "res", 0)
cache.Set("onedrive_source_0_123.jpg", "res", 1)
res, err := handler.Source(context.Background(), "123.jpg", url.URL{}, 0, true, 0)
cache.Deletes([]string{"0_123.jpg"}, "onedrive_source_")
asserts.NoError(err)
@@ -160,7 +161,7 @@ func TestDriver_Source(t *testing.T) {
handler.Client.Credential.ExpiresIn = time.Now().Add(time.Duration(100) * time.Hour).Unix()
handler.Client.Credential.AccessToken = "1"
cache.Set(fmt.Sprintf("onedrive_source_file_%d_1", file.UpdatedAt.Unix()), "res", 0)
res, err := handler.Source(ctx, "123.jpg", url.URL{}, 0, true, 0)
res, err := handler.Source(ctx, "123.jpg", url.URL{}, 1, true, 0)
cache.Deletes([]string{"0_123.jpg"}, "onedrive_source_")
asserts.NoError(err)
asserts.Equal("res", res)
@@ -185,10 +186,25 @@ func TestDriver_Source(t *testing.T) {
})
handler.Client.Request = clientMock
handler.Client.Credential.AccessToken = "1"
res, err := handler.Source(context.Background(), "123.jpg", url.URL{}, 0, true, 0)
res, err := handler.Source(context.Background(), "123.jpg", url.URL{}, 1, true, 0)
asserts.NoError(err)
asserts.Equal("123321", res)
}
// 成功 永久直链
{
file := model.File{}
file.ID = 1
file.Name = "123.jpg"
file.UpdatedAt = time.Now()
ctx := context.WithValue(context.Background(), fsctx.FileModelCtx, file)
handler.Client.Credential.ExpiresIn = time.Now().Add(time.Duration(100) * time.Hour).Unix()
auth.General = auth.HMACAuth{}
handler.Client.Credential.AccessToken = "1"
res, err := handler.Source(ctx, "123.jpg", url.URL{}, 0, true, 0)
asserts.NoError(err)
asserts.Contains(res, "/api/v3/file/source/1/123.jpg?sign")
}
}
func TestDriver_List(t *testing.T) {
+9 -11
View File
@@ -2,6 +2,8 @@ package filesystem
import (
"context"
"io"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
@@ -9,8 +11,6 @@ import (
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/juju/ratelimit"
"io"
"net/url"
)
/* ============
@@ -126,7 +126,7 @@ func (fs *FileSystem) Preview(ctx context.Context, id uint, isText bool) (*respo
// 否则重定向到签名的预览URL
ttl := model.GetIntSetting("preview_timeout", 60)
previewURL, err := fs.signURL(ctx, &fs.FileTarget[0], int64(ttl), false)
previewURL, err := fs.SignURL(ctx, &fs.FileTarget[0], int64(ttl), false)
if err != nil {
return nil, err
}
@@ -234,7 +234,7 @@ func (fs *FileSystem) GetDownloadURL(ctx context.Context, id uint, timeout strin
// 生成下載地址
ttl := model.GetIntSetting(timeout, 60)
source, err := fs.signURL(
source, err := fs.SignURL(
ctx,
fileTarget,
int64(ttl),
@@ -264,7 +264,7 @@ func (fs *FileSystem) GetSource(ctx context.Context, fileID uint) (string, error
)
}
source, err := fs.signURL(ctx, &fs.FileTarget[0], 0, false)
source, err := fs.SignURL(ctx, &fs.FileTarget[0], 0, false)
if err != nil {
return "", serializer.NewError(serializer.CodeNotSet, "无法获取外链", err)
}
@@ -272,7 +272,8 @@ func (fs *FileSystem) GetSource(ctx context.Context, fileID uint) (string, error
return source, nil
}
func (fs *FileSystem) signURL(ctx context.Context, file *model.File, ttl int64, isDownload bool) (string, error) {
// SignURL 签名文件原始 URL
func (fs *FileSystem) SignURL(ctx context.Context, file *model.File, ttl int64, isDownload bool) (string, error) {
fs.FileTarget = []model.File{*file}
ctx = context.WithValue(ctx, fsctx.FileModelCtx, *file)
@@ -288,11 +289,8 @@ func (fs *FileSystem) signURL(ctx context.Context, file *model.File, ttl int64,
if err != nil {
return "", serializer.NewError(serializer.CodeNotSet, "无法获取外链", err)
}
// 阿里云的 golang SDK 会把整个object KEY也编码 临时解决方案是清空`RawPath`让golang的`url.EscapedPath`修正这个问题
// https://github.com/cloudreve/Cloudreve/issues/677 https://github.com/aliyun/aliyun-oss-go-sdk/blob/6f7e8f88c64181cc2d86d8bd46090b68851e645a/oss/conn.go#L767
sourceUrl, _ := url.Parse(source)
sourceUrl.RawPath = ""
return sourceUrl.String(), nil
return source, nil
}
// ResetFileIfNotExist 重设当前目标文件为 path,如果当前目标为空
+30 -28
View File
@@ -4,20 +4,21 @@ import model "github.com/cloudreve/Cloudreve/v3/models"
// SiteConfig 站点全局设置序列
type SiteConfig struct {
SiteName string `json:"title"`
SiteICPId string `json:"siteICPId"`
LoginCaptcha bool `json:"loginCaptcha"`
RegCaptcha bool `json:"regCaptcha"`
ForgetCaptcha bool `json:"forgetCaptcha"`
EmailActive bool `json:"emailActive"`
Themes string `json:"themes"`
DefaultTheme string `json:"defaultTheme"`
HomepageViewMethod string `json:"home_view_method"`
ShareViewMethod string `json:"share_view_method"`
Authn bool `json:"authn"`
User User `json:"user"`
UseReCaptcha bool `json:"captcha_IsUseReCaptcha"`
ReCaptchaKey string `json:"captcha_ReCaptchaKey"`
SiteName string `json:"title"`
SiteICPId string `json:"siteICPId"`
LoginCaptcha bool `json:"loginCaptcha"`
RegCaptcha bool `json:"regCaptcha"`
ForgetCaptcha bool `json:"forgetCaptcha"`
EmailActive bool `json:"emailActive"`
Themes string `json:"themes"`
DefaultTheme string `json:"defaultTheme"`
HomepageViewMethod string `json:"home_view_method"`
ShareViewMethod string `json:"share_view_method"`
Authn bool `json:"authn"`
User User `json:"user"`
ReCaptchaKey string `json:"captcha_ReCaptchaKey"`
CaptchaType string `json:"captcha_type"`
TCaptchaCaptchaAppId string `json:"tcaptcha_captcha_app_id"`
}
type task struct {
@@ -64,20 +65,21 @@ func BuildSiteConfig(settings map[string]string, user *model.User) Response {
}
res := Response{
Data: SiteConfig{
SiteName: checkSettingValue(settings, "siteName"),
SiteICPId: checkSettingValue(settings, "siteICPId"),
LoginCaptcha: model.IsTrueVal(checkSettingValue(settings, "login_captcha")),
RegCaptcha: model.IsTrueVal(checkSettingValue(settings, "reg_captcha")),
ForgetCaptcha: model.IsTrueVal(checkSettingValue(settings, "forget_captcha")),
EmailActive: model.IsTrueVal(checkSettingValue(settings, "email_active")),
Themes: checkSettingValue(settings, "themes"),
DefaultTheme: checkSettingValue(settings, "defaultTheme"),
HomepageViewMethod: checkSettingValue(settings, "home_view_method"),
ShareViewMethod: checkSettingValue(settings, "share_view_method"),
Authn: model.IsTrueVal(checkSettingValue(settings, "authn_enabled")),
User: userRes,
UseReCaptcha: model.IsTrueVal(checkSettingValue(settings, "captcha_IsUseReCaptcha")),
ReCaptchaKey: checkSettingValue(settings, "captcha_ReCaptchaKey"),
SiteName: checkSettingValue(settings, "siteName"),
SiteICPId: checkSettingValue(settings, "siteICPId"),
LoginCaptcha: model.IsTrueVal(checkSettingValue(settings, "login_captcha")),
RegCaptcha: model.IsTrueVal(checkSettingValue(settings, "reg_captcha")),
ForgetCaptcha: model.IsTrueVal(checkSettingValue(settings, "forget_captcha")),
EmailActive: model.IsTrueVal(checkSettingValue(settings, "email_active")),
Themes: checkSettingValue(settings, "themes"),
DefaultTheme: checkSettingValue(settings, "defaultTheme"),
HomepageViewMethod: checkSettingValue(settings, "home_view_method"),
ShareViewMethod: checkSettingValue(settings, "share_view_method"),
Authn: model.IsTrueVal(checkSettingValue(settings, "authn_enabled")),
User: userRes,
ReCaptchaKey: checkSettingValue(settings, "captcha_ReCaptchaKey"),
CaptchaType: checkSettingValue(settings, "captcha_type"),
TCaptchaCaptchaAppId: checkSettingValue(settings, "captcha_TCaptcha_CaptchaAppId"),
}}
return res
}
+3 -3
View File
@@ -371,10 +371,10 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request, fs *filesyst
fs.Use("AfterValidateFailed", filesystem.HookDeleteTempFile)
fs.Use("AfterValidateFailed", filesystem.HookGiveBackCapacity)
fs.Use("AfterUploadFailed", filesystem.HookGiveBackCapacity)
}
// 禁止覆盖
ctx = context.WithValue(ctx, fsctx.DisableOverwrite, true)
// 禁止覆盖
ctx = context.WithValue(ctx, fsctx.DisableOverwrite, true)
}
// 执行上传
err = fs.Upload(ctx, fileData)
+1 -1
View File
@@ -25,7 +25,7 @@ func AdminSummary(c *gin.Context) {
// AdminNews 获取社区新闻
func AdminNews(c *gin.Context) {
r := request.HTTPClient{}
res := r.Request("GET", "https://forum.cloudreve.org/api/discussions?include=startUser%2ClastUser%2CstartPost%2Ctags&filter%5Bq%5D=%20tag%3Anotice&sort=-startTime&", nil)
res := r.Request("GET", "https://forum.cloudreve.org/api/discussions?include=startUser%2ClastUser%2CstartPost%2Ctags&filter%5Bq%5D=%20tag%3Anotice&sort=-startTime&page%5Blimit%5D=10", nil)
if res.Err == nil {
io.Copy(c.Writer, res.Response.Body)
}
+23
View File
@@ -88,6 +88,29 @@ func AnonymousGetContent(c *gin.Context) {
}
}
// AnonymousPermLink 文件签名后的永久链接
func AnonymousPermLink(c *gin.Context) {
// 创建上下文
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var service explorer.FileAnonymousGetService
if err := c.ShouldBindUri(&service); err == nil {
res := service.Source(ctx, c)
// 是否需要重定向
if res.Code == -302 {
c.Redirect(302, res.Data.(string))
return
}
// 是否有错误发生
if res.Code != 0 {
c.JSON(200, res)
}
} else {
c.JSON(200, ErrorResponse(err))
}
}
// GetSource 获取文件的外链地址
func GetSource(c *gin.Context) {
// 创建上下文
+2 -1
View File
@@ -24,8 +24,9 @@ func SiteConfig(c *gin.Context) {
"home_view_method",
"share_view_method",
"authn_enabled",
"captcha_IsUseReCaptcha",
"captcha_ReCaptchaKey",
"captcha_type",
"captcha_TCaptcha_CaptchaAppId",
)
// 如果已登录,则同时返回用户信息和标签
+6 -3
View File
@@ -116,16 +116,17 @@ func InitMasterRouter() *gin.Engine {
user := v3.Group("user")
{
// 用户登录
user.POST("session", controllers.UserLogin)
user.POST("session", middleware.CaptchaRequired("login_captcha"), controllers.UserLogin)
// 用户注册
user.POST("",
middleware.IsFunctionEnabled("register_enabled"),
middleware.CaptchaRequired("reg_captcha"),
controllers.UserRegister,
)
// 用二步验证户登录
user.POST("2fa", controllers.User2FALogin)
// 发送密码重设邮件
user.POST("reset", controllers.UserSendReset)
user.POST("reset", middleware.CaptchaRequired("forget_captcha"), controllers.UserSendReset)
// 通过邮件里的链接重设密码
user.PATCH("reset", controllers.UserReset)
// 邮件激活
@@ -162,8 +163,10 @@ func InitMasterRouter() *gin.Engine {
{
file := sign.Group("file")
{
// 文件外链
// 文件外链(直接输出文件数据)
file.GET("get/:id/:name", controllers.AnonymousGetContent)
// 文件外链(301跳转)
file.GET("source/:id/:name", controllers.AnonymousPermLink)
// 下載已经打包好的文件
file.GET("archive/:id/archive.zip", controllers.DownloadArchive)
// 下载文件
+9 -1
View File
@@ -207,7 +207,15 @@ func (service *OneDriveCallback) PreProcess(c *gin.Context) serializer.Response
// 验证与回调会话中是否一致
actualPath := strings.TrimPrefix(callbackSession.SavePath, "/")
if callbackSession.Size != info.Size || info.GetSourcePath() != actualPath {
isSizeCheckFailed := callbackSession.Size != info.Size
// SharePoint 会对 Office 文档增加 meta data 导致文件大小不一致,这里增加 10 KB 宽容
// See: https://github.com/OneDrive/onedrive-api-docs/issues/935
if strings.Contains(fs.Policy.OptionsSerialized.OdDriver, "sharepoint.com") && isSizeCheckFailed && (info.Size > callbackSession.Size) && (info.Size-callbackSession.Size <= 10240) {
isSizeCheckFailed = false
}
if isSizeCheckFailed || info.GetSourcePath() != actualPath {
fs.Handler.(onedrive.Driver).Client.Delete(context.Background(), []string{info.GetSourcePath()})
return serializer.Err(serializer.CodeUploadFailed, "文件信息不一致", err)
}
+27
View File
@@ -184,6 +184,33 @@ func (service *FileAnonymousGetService) Download(ctx context.Context, c *gin.Con
}
}
// Source 重定向到文件的有效原始链接
func (service *FileAnonymousGetService) Source(ctx context.Context, c *gin.Context) serializer.Response {
fs, err := filesystem.NewAnonymousFileSystem()
if err != nil {
return serializer.Err(serializer.CodeGroupNotAllowed, err.Error(), err)
}
defer fs.Recycle()
// 查找文件
err = fs.SetTargetFileByIDs([]uint{service.ID})
if err != nil {
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
// 获取文件流
res, err := fs.SignURL(ctx, &fs.FileTarget[0],
int64(model.GetIntSetting("preview_timeout", 60)), false)
if err != nil {
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
return serializer.Response{
Code: -302,
Data: res,
}
}
// CreateDocPreviewSession 创建DOC文件预览会话,返回预览地址
func (service *FileIDService) CreateDocPreviewSession(ctx context.Context, c *gin.Context) serializer.Response {
// 创建文件系统
+4 -55
View File
@@ -2,33 +2,27 @@ package user
import (
"fmt"
"net/url"
"time"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/email"
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
"github.com/cloudreve/Cloudreve/v3/pkg/recaptcha"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha"
"github.com/pquerna/otp/totp"
"net/url"
)
// UserLoginService 管理用户登录的服务
type UserLoginService struct {
//TODO 细致调整验证规则
UserName string `form:"userName" json:"userName" binding:"required,email"`
Password string `form:"Password" json:"Password" binding:"required,min=4,max=64"`
CaptchaCode string `form:"captchaCode" json:"captchaCode"`
UserName string `form:"userName" json:"userName" binding:"required,email"`
Password string `form:"Password" json:"Password" binding:"required,min=4,max=64"`
}
// UserResetEmailService 发送密码重设邮件服务
type UserResetEmailService struct {
UserName string `form:"userName" json:"userName" binding:"required,email"`
CaptchaCode string `form:"captchaCode" json:"captchaCode"`
UserName string `form:"userName" json:"userName" binding:"required,email"`
}
// UserResetService 密码重设服务
@@ -69,28 +63,6 @@ func (service *UserResetService) Reset(c *gin.Context) serializer.Response {
// Reset 发送密码重设邮件
func (service *UserResetEmailService) Reset(c *gin.Context) serializer.Response {
// 检查验证码
isCaptchaRequired := model.IsTrueVal(model.GetSettingByName("forget_captcha"))
useRecaptcha := model.IsTrueVal(model.GetSettingByName("captcha_IsUseReCaptcha"))
recaptchaSecret := model.GetSettingByName("captcha_ReCaptchaSecret")
if isCaptchaRequired && !useRecaptcha {
captchaID := util.GetSession(c, "captchaID")
util.DeleteSession(c, "captchaID")
if captchaID == nil || !base64Captcha.VerifyCaptcha(captchaID.(string), service.CaptchaCode) {
return serializer.ParamErr("验证码错误", nil)
}
} else if isCaptchaRequired && useRecaptcha {
captcha, err := recaptcha.NewReCAPTCHA(recaptchaSecret, recaptcha.V2, 10*time.Second)
if err != nil {
util.Log().Error(err.Error())
}
err = captcha.Verify(service.CaptchaCode)
if err != nil {
util.Log().Error(err.Error())
return serializer.ParamErr("验证失败,请刷新网页后再次验证", nil)
}
}
// 查找用户
if user, err := model.GetUserByEmail(service.UserName); err == nil {
@@ -151,30 +123,7 @@ func (service *Enable2FA) Login(c *gin.Context) serializer.Response {
// Login 用户登录函数
func (service *UserLoginService) Login(c *gin.Context) serializer.Response {
isCaptchaRequired := model.GetSettingByName("login_captcha")
useRecaptcha := model.GetSettingByName("captcha_IsUseReCaptcha")
recaptchaSecret := model.GetSettingByName("captcha_ReCaptchaSecret")
expectedUser, err := model.GetUserByEmail(service.UserName)
if (model.IsTrueVal(isCaptchaRequired)) && !(model.IsTrueVal(useRecaptcha)) {
// TODO 验证码校验
captchaID := util.GetSession(c, "captchaID")
util.DeleteSession(c, "captchaID")
if captchaID == nil || !base64Captcha.VerifyCaptcha(captchaID.(string), service.CaptchaCode) {
return serializer.ParamErr("验证码错误", nil)
}
} else if (model.IsTrueVal(isCaptchaRequired)) && (model.IsTrueVal(useRecaptcha)) {
captcha, err := recaptcha.NewReCAPTCHA(recaptchaSecret, recaptcha.V2, 10*time.Second)
if err != nil {
util.Log().Error(err.Error())
}
err = captcha.Verify(service.CaptchaCode)
if err != nil {
util.Log().Error(err.Error())
return serializer.ParamErr("验证失败,请刷新网页后再次验证", nil)
}
}
// 一系列校验
if err != nil {
return serializer.Err(serializer.CodeCredentialInvalid, "用户邮箱或密码错误", err)
+5 -32
View File
@@ -1,54 +1,27 @@
package user
import (
"net/url"
"strings"
"time"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
"github.com/cloudreve/Cloudreve/v3/pkg/email"
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
"github.com/cloudreve/Cloudreve/v3/pkg/recaptcha"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha"
"net/url"
"strings"
)
// UserRegisterService 管理用户注册的服务
type UserRegisterService struct {
//TODO 细致调整验证规则
UserName string `form:"userName" json:"userName" binding:"required,email"`
Password string `form:"Password" json:"Password" binding:"required,min=4,max=64"`
CaptchaCode string `form:"captchaCode" json:"captchaCode"`
UserName string `form:"userName" json:"userName" binding:"required,email"`
Password string `form:"Password" json:"Password" binding:"required,min=4,max=64"`
}
// Register 新用户注册
func (service *UserRegisterService) Register(c *gin.Context) serializer.Response {
// 相关设定
options := model.GetSettingByNames("email_active", "reg_captcha")
// 检查验证码
isCaptchaRequired := model.IsTrueVal(options["reg_captcha"])
useRecaptcha := model.IsTrueVal(model.GetSettingByName("captcha_IsUseReCaptcha"))
recaptchaSecret := model.GetSettingByName("captcha_ReCaptchaSecret")
if isCaptchaRequired && !useRecaptcha {
captchaID := util.GetSession(c, "captchaID")
util.DeleteSession(c, "captchaID")
if captchaID == nil || !base64Captcha.VerifyCaptcha(captchaID.(string), service.CaptchaCode) {
return serializer.ParamErr("验证码错误", nil)
}
} else if isCaptchaRequired && useRecaptcha {
captcha, err := recaptcha.NewReCAPTCHA(recaptchaSecret, recaptcha.V2, 10*time.Second)
if err != nil {
util.Log().Error(err.Error())
}
err = captcha.Verify(service.CaptchaCode)
if err != nil {
util.Log().Error(err.Error())
return serializer.ParamErr("验证失败,请刷新网页后再次验证", nil)
}
}
options := model.GetSettingByNames("email_active")
// 相关设定
isEmailRequired := model.IsTrueVal(options["email_active"])