Changing password and use cache when uploading

This commit is contained in:
Aurélie Delhaie
2022-05-29 17:56:03 +02:00
parent d0ee7df3f5
commit 7a8672ce80
7 changed files with 239 additions and 93 deletions

View File

@@ -8,6 +8,7 @@ database:
username: root username: root
features: features:
allow_register: false allow_register: false
password_hash_cost: 16
path: path:
cache: "/var/osc/cache" cache: "/var/osc/cache"
storage: "/var/osc/storage" storage: "/var/osc/storage"

View File

@@ -2,6 +2,7 @@ package config
import ( import (
"flag" "flag"
"golang.org/x/crypto/bcrypt"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"log" "log"
"os" "os"
@@ -32,6 +33,7 @@ type DatabaseConfiguration struct {
type FeaturesConfiguration struct { type FeaturesConfiguration struct {
AllowRegister bool `yaml:"allow_register"` AllowRegister bool `yaml:"allow_register"`
PasswordHashCost *int `yaml:"password_hash_cost"`
} }
var currentConfig *Configuration var currentConfig *Configuration
@@ -47,6 +49,22 @@ func init() {
if err != nil { if err != nil {
log.Fatalf("error: %s", err) log.Fatalf("error: %s", err)
} }
checkConfig()
}
func checkConfig() {
if currentConfig.Features.PasswordHashCost == nil {
currentConfig.Features.PasswordHashCost = new(int)
*currentConfig.Features.PasswordHashCost = bcrypt.DefaultCost
} else if *currentConfig.Features.PasswordHashCost < bcrypt.MinCost && *currentConfig.Features.PasswordHashCost > bcrypt.MaxCost {
log.Fatalf("password_hash_cost is not on the supported range (%d < x < %d)", bcrypt.MinCost, bcrypt.MaxCost)
}
if _, err := os.Stat(currentConfig.Path.Storage); err != nil {
log.Fatal(err)
}
if _, err := os.Stat(currentConfig.Path.Cache); err != nil {
log.Fatal(err)
}
} }
func Database() *DatabaseConfiguration { func Database() *DatabaseConfiguration {

View File

@@ -1,31 +1,23 @@
package database package database
import ( import (
"errors"
"fmt" "fmt"
"github.com/google/uuid" "github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"gorm.io/driver/mysql" "gorm.io/driver/mysql"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger" "gorm.io/gorm/logger"
"io"
"log" "log"
"mime/multipart"
"opensavecloudserver/config" "opensavecloudserver/config"
"os" "os"
"path"
"sync"
"time" "time"
) )
var (
locks map[int]GameUploadToken
mu sync.Mutex
)
var db *gorm.DB var db *gorm.DB
const adminRole string = "admin"
func init() { func init() {
locks = make(map[int]GameUploadToken)
dbConfig := config.Database() dbConfig := config.Database()
var err error var err error
connectionString := "" connectionString := ""
@@ -55,12 +47,6 @@ func init() {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
go func() {
for {
time.Sleep(time.Minute)
clearLocks()
}
}()
} }
// UserByUsername get a user by the username // UserByUsername get a user by the username
@@ -70,6 +56,9 @@ func UserByUsername(username string) (*User, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if user.Role == adminRole {
user.IsAdmin = true
}
return user, nil return user, nil
} }
@@ -80,6 +69,9 @@ func UserById(userId int) (*User, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if user.Role == adminRole {
user.IsAdmin = true
}
return user, nil return user, nil
} }
@@ -92,6 +84,16 @@ func AddUser(username string, password []byte) error {
return db.Save(user).Error return db.Save(user).Error
} }
// AddAdmin register a user and set his role to admin
func AddAdmin(username string, password []byte) error {
user := &User{
Username: username,
Password: password,
Role: adminRole,
}
return db.Save(user).Error
}
// GameInfoById return information of a game // GameInfoById return information of a game
func GameInfoById(userId, gameId int) (*Game, error) { func GameInfoById(userId, gameId int) (*Game, error) {
var game *Game var game *Game
@@ -128,51 +130,6 @@ func CreateGame(userId int, name string) (*Game, error) {
return game, nil return game, nil
} }
// AskForUpload Create a lock for upload a new revision of a game
func AskForUpload(userId, gameId int) (*GameUploadToken, error) {
mu.Lock()
defer mu.Unlock()
_, err := GameInfoById(userId, gameId)
if err != nil {
return nil, err
}
if _, ok := locks[gameId]; !ok {
token := uuid.New()
lock := GameUploadToken{
GameId: gameId,
UploadToken: token.String(),
}
locks[gameId] = lock
return &lock, nil
}
return nil, errors.New("game already locked")
}
func CheckUploadToken(uploadToken string) (int, bool) {
mu.Lock()
defer mu.Unlock()
for _, lock := range locks {
if lock.UploadToken == uploadToken {
return lock.GameId, true
}
}
return -1, false
}
func UploadSave(file multipart.File, game *Game) error {
filePath := path.Join(config.Path().Storage, game.PathStorage)
f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, file)
if err != nil {
return err
}
return nil
}
func UpdateGameRevision(game *Game, hash string) error { func UpdateGameRevision(game *Game, hash string) error {
game.Revision += 1 game.Revision += 1
if game.Hash == nil { if game.Hash == nil {
@@ -191,24 +148,20 @@ func UpdateGameRevision(game *Game, hash string) error {
return nil return nil
} }
func UnlockGame(gameId int) { // ChangePassword change the password of the user, the param 'password' must be the clear password
mu.Lock() func ChangePassword(userId int, password []byte) error {
defer mu.Unlock() user, err := UserById(userId)
delete(locks, gameId) if err != nil {
return err
} }
hashedPassword, err := bcrypt.GenerateFromPassword(password, *config.Features().PasswordHashCost)
// clearLocks clear lock of zombi upload if err != nil {
func clearLocks() { return err
mu.Lock()
defer mu.Unlock()
now := time.Now()
toUnlock := make([]int, 0)
for gameId, lock := range locks {
if lock.Expire.After(now) {
toUnlock = append(toUnlock, gameId)
} }
user.Password = hashedPassword
err = db.Save(user).Error
if err != nil {
return err
} }
for _, gameId := range toUnlock { return nil
delete(locks, gameId)
}
} }

View File

@@ -5,7 +5,9 @@ import "time"
type User struct { type User struct {
ID int `json:"id"` ID int `json:"id"`
Username string `json:"username"` Username string `json:"username"`
Role string `json:"role"`
Password []byte `json:"-"` Password []byte `json:"-"`
IsAdmin bool `json:"is_admin" gorm:"-:all"`
} }
type Game struct { type Game struct {
@@ -18,9 +20,3 @@ type Game struct {
UserId int `json:"-"` UserId int `json:"-"`
Available bool `json:"available"` Available bool `json:"available"`
} }
type GameUploadToken struct {
GameId int `json:"-"`
UploadToken string `json:"upload_token"`
Expire time.Time `json:"expire"`
}

View File

@@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"opensavecloudserver/config" "opensavecloudserver/config"
"opensavecloudserver/database" "opensavecloudserver/database"
"opensavecloudserver/upload"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@@ -27,6 +28,11 @@ type LockError struct {
Message string `json:"message"` Message string `json:"message"`
} }
type NewPassword struct {
Password string `json:"password"`
VerifyPassword string `json:"verify_password"`
}
// CreateGame create a game entry to the database // CreateGame create a game entry to the database
func CreateGame(w http.ResponseWriter, r *http.Request) { func CreateGame(w http.ResponseWriter, r *http.Request) {
userId, err := userIdFromContext(r.Context()) userId, err := userIdFromContext(r.Context())
@@ -119,7 +125,7 @@ func AskForUpload(w http.ResponseWriter, r *http.Request) {
log.Println(err) log.Println(err)
return return
} }
token, err := database.AskForUpload(userId, gameInfo.GameId) token, err := upload.AskForUpload(userId, gameInfo.GameId)
if err != nil { if err != nil {
ok(LockError{Message: err.Error()}, w, r) ok(LockError{Message: err.Error()}, w, r)
return return
@@ -141,7 +147,7 @@ func UploadSave(w http.ResponseWriter, r *http.Request) {
log.Println(err) log.Println(err)
return return
} }
defer database.UnlockGame(gameId) defer upload.UnlockGame(gameId)
hash := r.Header.Get("X-Game-Save-Hash") hash := r.Header.Get("X-Game-Save-Hash")
if utf8.RuneCountInString(hash) == 0 { if utf8.RuneCountInString(hash) == 0 {
badRequest("The header X-Game-Save-Hash is missing", w, r) badRequest("The header X-Game-Save-Hash is missing", w, r)
@@ -160,7 +166,7 @@ func UploadSave(w http.ResponseWriter, r *http.Request) {
return return
} }
defer file.Close() defer file.Close()
err = database.UploadSave(file, game) err = upload.UploadSave(file, game)
if err != nil { if err != nil {
internalServerError(w, r) internalServerError(w, r)
log.Println(err) log.Println(err)
@@ -194,7 +200,7 @@ func Download(w http.ResponseWriter, r *http.Request) {
log.Println(err) log.Println(err)
return return
} }
defer database.UnlockGame(gameId) defer upload.UnlockGame(gameId)
game, err := database.GameInfoById(userId, gameId) game, err := database.GameInfoById(userId, gameId)
if err != nil { if err != nil {
internalServerError(w, r) internalServerError(w, r)
@@ -237,3 +243,41 @@ func UserInformation(w http.ResponseWriter, r *http.Request) {
} }
ok(user, w, r) ok(user, w, r)
} }
func ChangePassword(w http.ResponseWriter, r *http.Request) {
userId, err := userIdFromContext(r.Context())
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
newPassword := new(NewPassword)
err = json.Unmarshal(body, newPassword)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
if newPassword.Password != newPassword.VerifyPassword {
badRequest("password are not the same", w, r)
return
}
err = database.ChangePassword(userId, []byte(newPassword.Password))
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
payload := &successMessage{
Message: "Password changed",
Timestamp: time.Now(),
Status: 200,
}
ok(payload, w, r)
}

View File

@@ -10,7 +10,7 @@ import (
"net/http" "net/http"
"opensavecloudserver/authentication" "opensavecloudserver/authentication"
"opensavecloudserver/config" "opensavecloudserver/config"
"opensavecloudserver/database" "opensavecloudserver/upload"
) )
type ContextKey string type ContextKey string
@@ -39,6 +39,7 @@ func Serve() {
r.Route("/user", func(secureRouter chi.Router) { r.Route("/user", func(secureRouter chi.Router) {
secureRouter.Use(authMiddleware) secureRouter.Use(authMiddleware)
secureRouter.Get("/information", UserInformation) secureRouter.Get("/information", UserInformation)
secureRouter.Post("/passwd", ChangePassword)
}) })
r.Route("/game", func(secureRouter chi.Router) { r.Route("/game", func(secureRouter chi.Router) {
secureRouter.Use(authMiddleware) secureRouter.Use(authMiddleware)
@@ -86,7 +87,7 @@ func uploadMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
header := r.Header.Get("X-Upload-Key") header := r.Header.Get("X-Upload-Key")
if len(header) > 0 { if len(header) > 0 {
if gameId, ok := database.CheckUploadToken(header); ok { if gameId, ok := upload.CheckUploadToken(header); ok {
ctx := context.WithValue(r.Context(), GameIdKey, gameId) ctx := context.WithValue(r.Context(), GameIdKey, gameId)
r = r.WithContext(ctx) r = r.WithContext(ctx)
next.ServeHTTP(w, r) next.ServeHTTP(w, r)

133
upload/upload.go Normal file
View File

@@ -0,0 +1,133 @@
package upload
import (
"errors"
"github.com/google/uuid"
"io"
"mime/multipart"
"opensavecloudserver/config"
"opensavecloudserver/database"
"os"
"path"
"sync"
"time"
)
var (
locks map[int]GameUploadToken
mu sync.Mutex
)
type GameUploadToken struct {
GameId int `json:"-"`
UploadToken string `json:"upload_token"`
Expire time.Time `json:"expire"`
}
func init() {
locks = make(map[int]GameUploadToken)
go func() {
for {
time.Sleep(time.Minute)
clearLocks()
}
}()
}
// AskForUpload Create a lock for upload a new revision of a game
func AskForUpload(userId, gameId int) (*GameUploadToken, error) {
mu.Lock()
defer mu.Unlock()
_, err := database.GameInfoById(userId, gameId)
if err != nil {
return nil, err
}
if _, ok := locks[gameId]; !ok {
token := uuid.New()
lock := GameUploadToken{
GameId: gameId,
UploadToken: token.String(),
}
locks[gameId] = lock
return &lock, nil
}
return nil, errors.New("game already locked")
}
func CheckUploadToken(uploadToken string) (int, bool) {
mu.Lock()
defer mu.Unlock()
for _, lock := range locks {
if lock.UploadToken == uploadToken {
return lock.GameId, true
}
}
return -1, false
}
func UploadSave(file multipart.File, game *database.Game) error {
filePath := path.Join(config.Path().Cache, string(rune(game.UserId)))
if _, err := os.Stat(filePath); err != nil {
err = os.Mkdir(filePath, 0766)
if err != nil {
return err
}
}
filePath = path.Join(filePath, game.PathStorage)
f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, file)
if err != nil {
return err
}
err = moveToStorage(filePath, game)
if err != nil {
return err
}
return nil
}
func moveToStorage(cachePath string, game *database.Game) error {
filePath := path.Join(config.Path().Storage, string(rune(game.UserId)))
if _, err := os.Stat(filePath); err != nil {
err = os.Mkdir(filePath, 0766)
if err != nil {
return err
}
}
filePath = path.Join(filePath, game.PathStorage)
if _, err := os.Stat(filePath); err == nil {
if err = os.Remove(filePath); err != nil {
return err
}
}
if err := os.Rename(cachePath, filePath); err != nil {
return err
}
return nil
}
func UnlockGame(gameId int) {
mu.Lock()
defer mu.Unlock()
delete(locks, gameId)
}
// clearLocks clear lock of zombi upload
func clearLocks() {
mu.Lock()
defer mu.Unlock()
now := time.Now()
toUnlock := make([]int, 0)
for gameId, lock := range locks {
if lock.Expire.After(now) {
toUnlock = append(toUnlock, gameId)
}
}
for _, gameId := range toUnlock {
delete(locks, gameId)
}
}