Skip to content

Commit

Permalink
Feat experimental WebAuth API
Browse files Browse the repository at this point in the history
  • Loading branch information
HFO4 committed Dec 8, 2019
1 parent 0932a10 commit f35c585
Show file tree
Hide file tree
Showing 14 changed files with 269 additions and 6 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.12

require (
github.com/DATA-DOG/go-sqlmock v1.3.3
github.com/duo-labs/webauthn v0.0.0-20191119193225-4bf9a0f776d4
github.com/fatih/color v1.7.0
github.com/garyburd/redigo v1.6.0
github.com/gin-contrib/cors v1.3.0
Expand Down
3 changes: 3 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"github.com/HFO4/cloudreve/models"
"github.com/HFO4/cloudreve/pkg/authn"
"github.com/HFO4/cloudreve/pkg/conf"
"github.com/HFO4/cloudreve/routers"
"github.com/gin-gonic/gin"
Expand All @@ -19,6 +20,8 @@ func init() {
if !conf.SystemConfig.Debug {
gin.SetMode(gin.ReleaseMode)
}

authn.Init()
}

func main() {
Expand Down
39 changes: 39 additions & 0 deletions models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package model

import (
"crypto/sha1"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/HFO4/cloudreve/pkg/util"
"github.com/duo-labs/webauthn/webauthn"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
"strings"
Expand Down Expand Up @@ -38,6 +41,7 @@ type User struct {
Delay int
Avatar string
Options string `json:"-",gorm:"size:4096"`
Authn string `gorm:"size:8192"`

// 关联模型
Group Group `gorm:"association_autoupdate:false"`
Expand All @@ -55,6 +59,41 @@ type UserOption struct {
PreferredTheme string `json:"preferred_theme"`
}

func (user User) WebAuthnID() []byte {
bs := make([]byte, 8)
binary.LittleEndian.PutUint64(bs, uint64(user.ID))
return bs
}

func (user User) WebAuthnName() string {
return user.Email
}

func (user User) WebAuthnDisplayName() string {
return user.Nick
}

func (user User) WebAuthnIcon() string {
return "https://cdn4.buysellads.net/uu/1/46074/1559075156-slack-carbon-red_2x.png"
}

func (user User) WebAuthnCredentials() []webauthn.Credential {
var res []webauthn.Credential
err := json.Unmarshal([]byte(user.Authn), &res)
if err != nil {
fmt.Println(err)
}
return res
}

func (user *User) RegisterAuthn(credential *webauthn.Credential) {
res, err := json.Marshal([]webauthn.Credential{*credential})
if err != nil {
fmt.Println(err)
}
DB.Model(user).UpdateColumn("authn", string(res))
}

// Root 获取用户的根目录
func (user *User) Root() (*Folder, error) {
var folder Folder
Expand Down
21 changes: 21 additions & 0 deletions pkg/authn/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package authn

import (
"fmt"
"github.com/duo-labs/webauthn/webauthn"
)

var Authn *webauthn.WebAuthn

func Init() {
var err error
Authn, err = webauthn.New(&webauthn.Config{
RPDisplayName: "Duo Labs", // Display Name for your site
RPID: "localhost", // Generally the FQDN for your site
RPOrigin: "http://localhost:3000", // The origin URL for WebAuthn requests
RPIcon: "https://duo.com/logo.png", // Optional icon URL for your site
})
if err != nil {
fmt.Println(err)
}
}
3 changes: 3 additions & 0 deletions pkg/filesystem/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"github.com/HFO4/cloudreve/models"
"github.com/HFO4/cloudreve/pkg/filesystem/local"
"github.com/HFO4/cloudreve/pkg/filesystem/response"
"github.com/gin-gonic/gin"
"io"
)
Expand All @@ -27,6 +28,8 @@ type Handler interface {
Delete(ctx context.Context, files []string) ([]string, error)
// 获取文件
Get(ctx context.Context, path string) (io.ReadSeeker, error)
// 获取缩略图
Thumb(ctx context.Context, path string) (*response.ContentResponse, error)
}

// FileSystem 管理文件的文件系统
Expand Down
22 changes: 21 additions & 1 deletion pkg/filesystem/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
model "github.com/HFO4/cloudreve/models"
"github.com/HFO4/cloudreve/pkg/filesystem/response"
"github.com/HFO4/cloudreve/pkg/thumb"
"github.com/HFO4/cloudreve/pkg/util"
)
Expand All @@ -16,6 +17,24 @@ import (
// HandledExtension 可以生成缩略图的文件扩展名
var HandledExtension = []string{"jpg", "jpeg", "png", "gif"}

// GetThumb 获取文件的缩略图
func (fs *FileSystem) GetThumb(ctx context.Context, id uint) (*response.ContentResponse, error) {
// 根据 ID 查找文件
file, err := model.GetFilesByIDs([]uint{id}, fs.User.ID)
if err != nil || len(file) == 0 || file[0].PicInfo == "" {
return &response.ContentResponse{
Redirect: false,
}, ErrObjectNotExist
}

fs.FileTarget = []model.File{file[0]}

res, err := fs.Handler.Thumb(ctx, file[0].SourceName)

// TODO 出错时重新生成缩略图
return res, err
}

// GenerateThumbnail 尝试为本地策略文件生成缩略图并获取图像原始大小
func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) {
// 判断是否可以生成缩略图
Expand Down Expand Up @@ -61,6 +80,7 @@ func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) {
}

// GenerateThumbnailSize 获取要生成的缩略图的尺寸
// TODO 从配置文件读取
func (fs *FileSystem) GenerateThumbnailSize(w, h int) (uint, uint) {
return 230, 200
return 400, 300
}
14 changes: 14 additions & 0 deletions pkg/filesystem/local/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package local

import (
"context"
"github.com/HFO4/cloudreve/pkg/filesystem/response"
"github.com/HFO4/cloudreve/pkg/util"
"io"
"os"
Expand Down Expand Up @@ -82,3 +83,16 @@ func (handler Handler) Delete(ctx context.Context, files []string) ([]string, er

return deleteFailed, retErr
}

// Thumb 获取文件缩略图
func (handler Handler) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) {
file, err := handler.Get(ctx, path+"._thumb")
if err != nil {
return nil, err
}

return &response.ContentResponse{
Redirect: false,
Content: file,
}, nil
}
12 changes: 12 additions & 0 deletions pkg/filesystem/response/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package response

import "io"

// ContentResponse 获取文件内容类方法的通用返回值。
// 有些上传策略需要重定向,
// 有些直接写文件数据到浏览器
type ContentResponse struct {
Redirect bool
Content io.ReadSeeker
URL string
}
6 changes: 6 additions & 0 deletions pkg/filesystem/upload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
model "github.com/HFO4/cloudreve/models"
"github.com/HFO4/cloudreve/pkg/filesystem/local"
"github.com/HFO4/cloudreve/pkg/filesystem/response"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -34,6 +35,11 @@ func (m FileHeaderMock) Delete(ctx context.Context, files []string) ([]string, e
return args.Get(0).([]string), args.Error(1)
}

func (m FileHeaderMock) Thumb(ctx context.Context, files string) (*response.ContentResponse, error) {
args := m.Called(ctx, files)
return args.Get(0).(*response.ContentResponse), args.Error(1)
}

func TestFileSystem_Upload(t *testing.T) {
asserts := assert.New(t)

Expand Down
2 changes: 1 addition & 1 deletion pkg/thumb/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func (image *Thumb) Save(path string) (err error) {
return err
}

err = jpeg.Encode(out, image.src, nil)
err = png.Encode(out, image.src)
return err

}
36 changes: 36 additions & 0 deletions routers/controllers/file.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package controllers

import "C"
import (
"context"
"github.com/HFO4/cloudreve/models"
Expand All @@ -9,10 +10,45 @@ import (
"github.com/HFO4/cloudreve/pkg/util"
"github.com/HFO4/cloudreve/service/explorer"
"github.com/gin-gonic/gin"
"net/http"
"net/url"
"strconv"
)

// Thumb 获取文件缩略图
func Thumb(c *gin.Context) {
// 创建上下文
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
c.JSON(200, serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err))
return
}

// 获取文件ID
fileID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(200, serializer.ParamErr("无法解析文件ID", err))
return
}

// 获取缩略图
resp, err := fs.GetThumb(ctx, uint(fileID))
if err != nil {
c.JSON(200, serializer.Err(serializer.CodeNotSet, "无法获取缩略图", err))
return
}

if resp.Redirect {
c.Redirect(http.StatusMovedPermanently, resp.URL)
return
}
http.ServeContent(c.Writer, c.Request, "thumb.png", fs.FileTarget[0].UpdatedAt, resp.Content)

}

// Download 文件下载
func Download(c *gin.Context) {
// 创建上下文
Expand Down
98 changes: 98 additions & 0 deletions routers/controllers/user.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,109 @@
package controllers

import (
"encoding/json"
model "github.com/HFO4/cloudreve/models"
"github.com/HFO4/cloudreve/pkg/authn"
"github.com/HFO4/cloudreve/pkg/serializer"
"github.com/HFO4/cloudreve/pkg/util"
"github.com/HFO4/cloudreve/service/user"
"github.com/duo-labs/webauthn/webauthn"
"github.com/gin-gonic/gin"
)

// StartLoginAuthn 开始注册WebAuthn登录
func StartLoginAuthn(c *gin.Context) {
userName := c.Param("username")
expectedUser, err := model.GetUserByEmail(userName)
if err != nil {
c.JSON(200, serializer.Err(401, "用户邮箱或密码错误", err))
return
}

options, sessionData, err := authn.Authn.BeginLogin(expectedUser)
if err != nil {
c.JSON(200, ErrorResponse(err))
return
}

val, err := json.Marshal(sessionData)
if err != nil {
c.JSON(200, ErrorResponse(err))
return
}

util.SetSession(c, map[string]interface{}{
"registration-session": val,
})
c.JSON(200, serializer.Response{Code: 0, Data: options})
}

// FinishLoginAuthn 完成注册WebAuthn登录
func FinishLoginAuthn(c *gin.Context) {
userName := c.Param("username")
expectedUser, err := model.GetUserByEmail(userName)
if err != nil {
c.JSON(200, serializer.Err(401, "用户邮箱或密码错误", err))
return
}

sessionDataJSON := util.GetSession(c, "registration-session").([]byte)

var sessionData webauthn.SessionData
err = json.Unmarshal(sessionDataJSON, &sessionData)

_, err = authn.Authn.FinishLogin(expectedUser, sessionData, c.Request)

if err != nil {
c.JSON(200, serializer.Err(401, "用户邮箱或密码错误", err))
return
}

util.SetSession(c, map[string]interface{}{
"user_id": expectedUser.ID,
})
c.JSON(200, serializer.BuildUserResponse(expectedUser))
}

// StartRegAuthn 开始注册WebAuthn信息
func StartRegAuthn(c *gin.Context) {
currUser := CurrentUser(c)
options, sessionData, err := authn.Authn.BeginRegistration(currUser)
if err != nil {
c.JSON(200, ErrorResponse(err))
return
}

val, err := json.Marshal(sessionData)
if err != nil {
c.JSON(200, ErrorResponse(err))
return
}

util.SetSession(c, map[string]interface{}{
"registration-session": val,
})
c.JSON(200, serializer.Response{Code: 0, Data: options})
}

// FinishRegAuthn 完成注册WebAuthn信息
func FinishRegAuthn(c *gin.Context) {
currUser := CurrentUser(c)
sessionDataJSON := util.GetSession(c, "registration-session").([]byte)

var sessionData webauthn.SessionData
err := json.Unmarshal(sessionDataJSON, &sessionData)

credential, err := authn.Authn.FinishRegistration(currUser, sessionData, c.Request)

currUser.RegisterAuthn(credential)
if err != nil {
c.JSON(200, ErrorResponse(err))
return
}
c.JSON(200, serializer.Response{Code: 0})
}

// UserLogin 用户登录
func UserLogin(c *gin.Context) {
var service user.UserLoginService
Expand Down
Loading

0 comments on commit f35c585

Please sign in to comment.