Start refactoring

This commit is contained in:
Aurélie Delhaie
2023-05-29 17:44:50 +02:00
parent 55ac50f3be
commit c06843cd28
31 changed files with 1125 additions and 1267 deletions

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@ storage/
.idea/ .idea/
build build
*.log *.log
id_rsa*

View File

@@ -1,29 +0,0 @@
package admin
import (
"opensavecloudserver/database"
"opensavecloudserver/upload"
)
// RemoveUser rome the user from the db and all his datas
func RemoveUser(user *database.User) error {
if err := database.RemoveAllUserGameEntries(user); err != nil {
return err
}
if err := upload.RemoveFolders(user.ID); err != nil {
return err
}
return database.RemoveUser(user)
}
func SetAdmin(user *database.User) error {
user.Role = database.AdminRole
user.IsAdmin = true
return database.SaveUser(user)
}
func RemoveAdminRole(user *database.User) error {
user.Role = database.UserRole
user.IsAdmin = false
return database.SaveUser(user)
}

View File

@@ -1,79 +0,0 @@
package authentication
import (
"crypto/rand"
"errors"
"github.com/golang-jwt/jwt"
"golang.org/x/crypto/bcrypt"
"log"
"opensavecloudserver/database"
)
var secret []byte
type AccessToken struct {
Token string `json:"token"`
}
type Registration struct {
Username string `json:"username"`
Password string `json:"password"`
}
func init() {
secret = make([]byte, 512)
_, err := rand.Read(secret)
if err != nil {
log.Fatal(err)
}
}
func Connect(username, password string) (*AccessToken, error) {
user, err := database.UserByUsername(username)
if err != nil {
return nil, err
}
if err := bcrypt.CompareHashAndPassword(user.Password, []byte(password)); err != nil {
return nil, err
}
token, err := token(user.ID)
if err != nil {
return nil, err
}
return &AccessToken{
Token: token,
}, nil
}
func ParseToken(token string) (int, error) {
var claims jwt.MapClaims
_, err := jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) {
return secret, nil
})
if err != nil {
return 0, err
}
if userId, ok := claims["sub"]; ok {
return int(userId.(float64)), nil
}
return 0, errors.New("this token does not have a userId in it")
}
func Register(user *Registration) error {
_, err := database.UserByUsername(user.Username)
if err == nil {
return errors.New("this username already exist")
}
hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 12)
if err != nil {
return err
}
return database.AddUser(user.Username, hash)
}
func token(userId int) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{
"sub": userId,
})
return token.SignedString(secret)
}

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
platforms=("windows/amd64" "linux/amd64" "linux/arm64" "linux/arm") platforms=("windows/amd64" "linux/amd64" "linux/arm64")
if [[ -d "./build" ]] if [[ -d "./build" ]]
then then
@@ -20,9 +20,6 @@ do
output_name+='.exe' output_name+='.exe'
go generate go generate
env GOAMD64=v3 GOOS=$GOOS GOARCH=$GOARCH CGO_ENABLED=1 go build -o $output_name -a env GOAMD64=v3 GOOS=$GOOS GOARCH=$GOARCH CGO_ENABLED=1 go build -o $output_name -a
else
if [ $GOARCH = "arm" ]; then
env GOARM=7 GOOS=$GOOS GOARCH=$GOARCH CGO_ENABLED=0 go build -o $output_name -a
else else
if [ $GOARCH = "amd64" ]; then if [ $GOARCH = "amd64" ]; then
env GOAMD64=v3 GOOS=$GOOS GOARCH=$GOARCH CGO_ENABLED=0 go build -o $output_name -a env GOAMD64=v3 GOOS=$GOOS GOARCH=$GOARCH CGO_ENABLED=0 go build -o $output_name -a
@@ -30,6 +27,5 @@ do
env GOOS=$GOOS GOARCH=$GOARCH CGO_ENABLED=0 go build -o $output_name -a env GOOS=$GOOS GOARCH=$GOARCH CGO_ENABLED=0 go build -o $output_name -a
fi fi
fi fi
fi
done done

View File

@@ -3,24 +3,17 @@ package main
import ( import (
"io" "io"
"log" "log"
"opensavecloudserver/config"
"opensavecloudserver/database"
"os" "os"
) )
func InitCommon() { func initLogger() (err error) {
f, err := os.OpenFile("server.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) f, err := os.OpenFile("server.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil { if err != nil {
log.Fatalf("error opening file: %v", err) return err
} }
defer func(f *os.File) { defer func(f *os.File) {
err := f.Close() err = f.Close()
if err != nil {
log.Println(err)
}
}(f) }(f)
log.SetOutput(io.MultiWriter(os.Stdout, f)) log.SetOutput(io.MultiWriter(os.Stdout, f))
return nil
config.Init()
database.Init()
} }

View File

@@ -1,13 +1,12 @@
--- ---
server: server:
port: 8080 port: 8080
ping: no
compress: no
database: database:
host: localhost url: "postgres://username:password@localhost:5432/database_name"
password: root
port: 3306
username: root
features: features:
allow_register: false allow_register: no
password_hash_cost: 16 password_hash_cost: 16
path: path:
cache: "/var/osc/cache" cache: "/var/osc/cache"

View File

@@ -1,84 +1,69 @@
package config package config
import ( import (
"flag" "fmt"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"log"
"os" "os"
) )
type Configuration struct { type (
Configuration struct {
Server ServerConfiguration `yaml:"server"` Server ServerConfiguration `yaml:"server"`
Database DatabaseConfiguration `yaml:"database"` Database DatabaseConfiguration `yaml:"database"`
Features FeaturesConfiguration `yaml:"features"` Features FeaturesConfiguration `yaml:"features"`
Path PathConfiguration `yaml:"path"` Path PathConfiguration `yaml:"path"`
} }
type PathConfiguration struct { PathConfiguration struct {
Cache string `yaml:"cache"` Cache string `yaml:"cache"`
Storage string `yaml:"storage"` Storage string `yaml:"storage"`
RSAKey string `yaml:"rsa_key"`
} }
type ServerConfiguration struct { ServerConfiguration struct {
Port int `yaml:"port"` Port int `yaml:"port"`
PingEndPoint bool `yaml:"ping"`
Compress bool `yaml:"compress"`
} }
type DatabaseConfiguration struct { DatabaseConfiguration struct {
Host string `yaml:"host"` URL string `yaml:"url"`
Port int `yaml:"port"`
Username string `yaml:"username"`
Password *string `yaml:"password"`
} }
type FeaturesConfiguration struct { FeaturesConfiguration struct {
AllowRegister bool `yaml:"allow_register"` AllowRegister bool `yaml:"allow_register"`
PasswordHashCost *int `yaml:"password_hash_cost"` PasswordHashCost *int `yaml:"password_hash_cost"`
} }
)
var currentConfig *Configuration func Load(path string) (Configuration, error) {
configYamlContent, err := os.ReadFile(path)
func Init() {
path := flag.String("config", "./config.yml", "Set the configuration file path")
flag.Parse()
configYamlContent, err := os.ReadFile(*path)
if err != nil { if err != nil {
log.Fatal(err) return Configuration{}, err
} }
err = yaml.Unmarshal(configYamlContent, &currentConfig) var config Configuration
if err != nil { if err := yaml.Unmarshal(configYamlContent, &config); err != nil {
log.Fatalf("error: %s", err) return Configuration{}, err
} }
checkConfig() if err := checkConfig(config); err != nil {
return Configuration{}, err
}
return config, nil
} }
func checkConfig() { func checkConfig(c Configuration) error {
if currentConfig.Features.PasswordHashCost == nil { if c.Features.PasswordHashCost == nil {
currentConfig.Features.PasswordHashCost = new(int) c.Features.PasswordHashCost = new(int)
*currentConfig.Features.PasswordHashCost = bcrypt.DefaultCost *c.Features.PasswordHashCost = bcrypt.DefaultCost
} else if *currentConfig.Features.PasswordHashCost < bcrypt.MinCost && *currentConfig.Features.PasswordHashCost > bcrypt.MaxCost { } else if *c.Features.PasswordHashCost < bcrypt.MinCost && *c.Features.PasswordHashCost > bcrypt.MaxCost {
log.Fatalf("password_hash_cost is not on the supported range (%d < x < %d)", bcrypt.MinCost, bcrypt.MaxCost) return fmt.Errorf("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 { if _, err := os.Stat(c.Path.Storage); err != nil {
log.Fatal(err) return nil
} }
if _, err := os.Stat(currentConfig.Path.Cache); err != nil { if _, err := os.Stat(c.Path.Cache); err != nil {
log.Fatal(err) return err
} }
} return nil
func Database() *DatabaseConfiguration {
return &currentConfig.Database
}
func Features() *FeaturesConfiguration {
return &currentConfig.Features
}
func Path() *PathConfiguration {
return &currentConfig.Path
}
func Server() *ServerConfiguration {
return &currentConfig.Server
} }

118
config/security/security.go Normal file
View File

@@ -0,0 +1,118 @@
package security
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"golang.org/x/crypto/ssh"
"log"
"os"
"path"
)
const (
defaultPrivateKeyName = "id_rsa"
defaultPublicKeyName = "id_rsa.pub"
)
// GenerateNewRSAKey generates a new RSA key, writes it to a file and return the private key
func GenerateNewRSAKey(savePath string, bitSize uint16) (*rsa.PrivateKey, error) {
pkPath := path.Join(savePath, defaultPrivateKeyName)
pubPath := path.Join(savePath, defaultPublicKeyName)
privateKey, err := generatePrivateKey(int(bitSize))
if err != nil {
return nil, err
}
publicKeyBytes, err := generatePublicKey(&privateKey.PublicKey)
if err != nil {
return nil, err
}
privateKeyBytes := encodePrivateKeyToPEM(privateKey)
if err := writeKeyToFile(privateKeyBytes, pkPath); err != nil {
return nil, err
}
if err := writeKeyToFile(publicKeyBytes, pubPath); err != nil {
return nil, err
}
return privateKey, nil
}
func LoadPrivateKey(folderPath string) (*rsa.PrivateKey, error) {
pkPath := path.Join(folderPath, defaultPrivateKeyName)
f, err := os.ReadFile(pkPath)
if err != nil {
return nil, err
}
block, _ := pem.Decode(f)
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
return key, nil
}
func generatePrivateKey(bitSize int) (*rsa.PrivateKey, error) {
// Private Key generation
privateKey, err := rsa.GenerateKey(rand.Reader, bitSize)
if err != nil {
return nil, err
}
// Validate Private Key
err = privateKey.Validate()
if err != nil {
return nil, err
}
log.Println("Private Key generated")
return privateKey, nil
}
// encodePrivateKeyToPEM encodes Private Key from RSA to PEM format
func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte {
// Get ASN.1 DER format
privDER := x509.MarshalPKCS1PrivateKey(privateKey)
// pem.Block
privBlock := pem.Block{
Type: "RSA PRIVATE KEY",
Headers: nil,
Bytes: privDER,
}
// Private key in PEM format
privatePEM := pem.EncodeToMemory(&privBlock)
return privatePEM
}
// generatePublicKey take a rsa.PublicKey and return bytes suitable for writing to .pub file
// returns in the format "ssh-rsa ..."
func generatePublicKey(privatekey *rsa.PublicKey) ([]byte, error) {
publicRsaKey, err := ssh.NewPublicKey(privatekey)
if err != nil {
return nil, err
}
pubKeyBytes := ssh.MarshalAuthorizedKey(publicRsaKey)
log.Println("Public key generated")
return pubKeyBytes, nil
}
// writePemToFile writes keys to a file
func writeKeyToFile(keyBytes []byte, saveFileTo string) error {
err := os.WriteFile(saveFileTo, keyBytes, 0600)
if err != nil {
return err
}
log.Printf("Key saved to: %s", saveFileTo)
return nil
}

View File

@@ -1,5 +1,5 @@
package constant package constant
const Version = "1.0.0" const Version = "2.0.0"
const ApiVersion = 1 const ApiVersion = 2

View File

@@ -0,0 +1,12 @@
package datasource
import (
"gorm.io/gorm"
)
type (
Datasource interface {
Connect(dsn string) error
DB() *gorm.DB
}
)

View File

@@ -0,0 +1,40 @@
package game
import (
"gorm.io/gorm"
"opensavecloudserver/data/datasource"
"opensavecloudserver/data/repository/game"
"opensavecloudserver/data/repository/user"
)
type (
GameDatasource struct {
db *gorm.DB
}
)
func (g *GameDatasource) GameMetadataByID(ID game.ID) (game.GameMetadata, error) {
//TODO implement me
panic("implement me")
}
func (g *GameDatasource) CreateGameEntry(game game.NewGameEntry) (game.GameMetadata, error) {
//TODO implement me
panic("implement me")
}
func (g *GameDatasource) GameSavesHistory(gameID game.ID) ([]game.GameSaveVersion, error) {
//TODO implement me
panic("implement me")
}
func (g *GameDatasource) UserGamesByUserID(userID user.ID) ([]game.GameMetadata, error) {
//TODO implement me
panic("implement me")
}
func NewGameDatasource(dts datasource.Datasource) game.GameRepository {
g := new(GameDatasource)
g.db = dts.DB()
return g
}

View File

@@ -0,0 +1,31 @@
package user
import (
"gorm.io/gorm"
"opensavecloudserver/data/datasource"
"opensavecloudserver/data/repository/user"
)
type (
UserDatasource struct {
db *gorm.DB
}
)
func NewUserDatasource(dts datasource.Datasource) user.UserRepository {
repo := new(UserDatasource)
repo.db = dts.DB()
return repo
}
func (ud *UserDatasource) UserByID(ID user.ID) (user.User, error) {
return nil, user.ErrUserNotFound
}
func (ud *UserDatasource) UserByUsername(username string) (user.User, error) {
return nil, user.ErrUserNotFound
}
func (ud *UserDatasource) CreateUser(u user.NewUserTemplate) (user.User, error) {
return nil, nil
}

View File

@@ -0,0 +1,30 @@
package pgsql
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
"opensavecloudserver/data/datasource"
)
type (
DatabaseDatasource struct {
conn *gorm.DB
}
)
func NewDatabaseDatasource() datasource.Datasource {
return new(DatabaseDatasource)
}
func (dd *DatabaseDatasource) Connect(dsn string) error {
conn, err := gorm.Open(postgres.Open(dsn))
if err != nil {
return err
}
dd.conn = conn
return nil
}
func (dd *DatabaseDatasource) DB() *gorm.DB {
return dd.conn
}

View File

@@ -0,0 +1,40 @@
package game
import (
"errors"
"github.com/google/uuid"
"opensavecloudserver/data/repository/user"
"time"
)
type (
GameRepository interface {
GameMetadataByID(ID ID) (GameMetadata, error)
CreateGameEntry(game NewGameEntry) (GameMetadata, error)
GameSavesHistory(gameID ID) ([]GameSaveVersion, error)
UserGamesByUserID(userID user.ID) ([]GameMetadata, error)
}
GameMetadata interface {
ID() uuid.UUID
Name() string
Path() string
Revision() string
}
GameSaveVersion interface {
ID() string
Date() time.Time
}
NewGameEntry interface {
Path() string
Name() string
}
ID uuid.UUID
)
var (
ErrGameNotFound = errors.New("game not found")
)

View File

@@ -0,0 +1,36 @@
package user
import (
"errors"
"github.com/google/uuid"
)
type (
UserRepository interface {
UserByID(ID ID) (User, error)
UserByUsername(username string) (User, error)
CreateUser(user NewUserTemplate) (User, error)
}
User interface {
ID() uuid.UUID
Username() string
DisplayName() string
Roles() []Role
SetPassword() string
CheckPassword(password string) bool
}
NewUserTemplate interface {
Username() string
Password() string
DisplayName() string
}
ID uuid.UUID
Role string
)
var (
ErrUserNotFound = errors.New("user not found")
)

View File

@@ -1,207 +0,0 @@
package database
import (
"fmt"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"log"
"opensavecloudserver/config"
"os"
"time"
)
var db *gorm.DB
const AdminRole string = "admin"
const UserRole string = "user"
func Init() {
dbConfig := config.Database()
var err error
connectionString := ""
if dbConfig.Password != nil {
connectionString = fmt.Sprintf("%s:%s@tcp(%s:%d)/osc?charset=utf8mb4&parseTime=True&loc=Local",
dbConfig.Username,
*dbConfig.Password,
dbConfig.Host,
dbConfig.Port)
} else {
connectionString = fmt.Sprintf("%s@tcp(%s:%d)/osc?charset=utf8mb4&parseTime=True&loc=Local",
dbConfig.Username,
dbConfig.Host,
dbConfig.Port)
}
db, err = gorm.Open(mysql.Open(connectionString), &gorm.Config{
Logger: logger.New(
log.New(os.Stdout, "", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Error, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
Colorful: true, // Enable color
},
),
})
if err != nil {
log.Fatal(err)
}
}
func AllUsers() ([]*User, error) {
var users []*User
err := db.Model(User{}).Find(&users).Error
if err != nil {
return nil, err
}
for _, user := range users {
if user.Role == AdminRole {
user.IsAdmin = true
}
}
return users, nil
}
// UserByUsername get a user by the username
func UserByUsername(username string) (*User, error) {
var user *User
err := db.Model(User{}).Where(User{Username: username}).First(&user).Error
if err != nil {
return nil, err
}
if user.Role == AdminRole {
user.IsAdmin = true
}
return user, nil
}
func ChangeUsername(userId int, newUsername string) error {
user, err := UserById(userId)
if err != nil {
return err
}
user.Username = newUsername
return db.Save(user).Error
}
// UserById get a user
func UserById(userId int) (*User, error) {
var user *User
err := db.Model(User{}).Where(userId).First(&user).Error
if err != nil {
return nil, err
}
if user.Role == AdminRole {
user.IsAdmin = true
}
return user, nil
}
// AddUser register a user
func AddUser(username string, password []byte) error {
user := &User{
Username: username,
Password: password,
}
return db.Save(user).Error
}
func SaveUser(user *User) error {
return db.Save(user).Error
}
func RemoveUser(user *User) error {
return db.Delete(User{}, user.ID).Error
}
func RemoveAllUserGameEntries(user *User) error {
return db.Delete(Game{}, Game{UserId: user.ID}).Error
}
func RemoveGame(game *Game) error {
return db.Delete(Game{}, Game{UserId: game.UserId, ID: game.ID}).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
func GameInfoById(userId, gameId int) (*Game, error) {
var game *Game
err := db.Model(Game{}).Where(Game{ID: gameId, UserId: userId}).First(&game).Error
if err != nil {
return nil, err
}
return game, nil
}
// GameInfosByUserId get all saved games for a user
func GameInfosByUserId(userId int) ([]*Game, error) {
var games []*Game
err := db.Model(Game{}).Where(Game{UserId: userId}).Find(&games).Error
if err != nil {
return nil, err
}
return games, nil
}
// CreateGame create an entry for a new game save, do this only for create a new entry
func CreateGame(userId int, name string) (*Game, error) {
gameUUID := uuid.New()
game := &Game{
Name: name,
Revision: 0,
PathStorage: gameUUID.String() + ".bin",
UserId: userId,
Available: false,
}
if err := db.Save(&game).Error; err != nil {
return nil, err
}
return game, nil
}
func UpdateGameRevision(game *Game, hash string) error {
game.Revision += 1
if game.Hash == nil {
game.Hash = new(string)
}
*game.Hash = hash
game.Available = true
if game.LastUpdate == nil {
game.LastUpdate = new(time.Time)
}
*game.LastUpdate = time.Now()
err := db.Save(game).Error
if err != nil {
return err
}
return nil
}
// ChangePassword change the password of the user, the param 'password' must be the clear password
func ChangePassword(userId int, password []byte) error {
user, err := UserById(userId)
if err != nil {
return err
}
hashedPassword, err := bcrypt.GenerateFromPassword(password, *config.Features().PasswordHashCost)
if err != nil {
return err
}
user.Password = hashedPassword
err = db.Save(user).Error
if err != nil {
return err
}
return nil
}

View File

@@ -1,22 +0,0 @@
package database
import "time"
type User struct {
Username string `json:"username"`
Role string `json:"role"`
Password []byte `json:"-"`
ID int `json:"id"`
IsAdmin bool `json:"is_admin" gorm:"-:all"`
}
type Game struct {
Name string `json:"name"`
PathStorage string `json:"-"`
ID int `json:"id"`
Revision int `json:"rev"`
UserId int `json:"-"`
Available bool `json:"available"`
Hash *string `json:"hash"`
LastUpdate *time.Time `json:"last_update"`
}

55
go.mod
View File

@@ -1,29 +1,50 @@
module opensavecloudserver module opensavecloudserver
go 1.18 go 1.20
require ( require (
github.com/getlantern/systray v1.2.1 github.com/getlantern/systray v1.2.1
github.com/go-chi/chi/v5 v5.0.7 github.com/go-chi/chi/v5 v5.0.8
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 github.com/lestrrat-go/jwx/v2 v2.0.9
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b golang.org/x/crypto v0.8.0
gorm.io/driver/mysql v1.3.3 gopkg.in/yaml.v3 v3.0.1
gorm.io/gorm v1.23.5 gorm.io/driver/postgres v1.5.2
gorm.io/gorm v1.25.0
tawesoft.co.uk/go/dialog v0.0.0-20201103210221-4175697d086f
) )
require ( require (
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 // indirect
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect github.com/getlantern/errors v1.0.3 // indirect
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect github.com/getlantern/golog v0.0.0-20230206140254-6d0a2e0f79af // indirect
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc // indirect
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect github.com/getlantern/ops v0.0.0-20220713155959-1315d978fff7 // indirect
github.com/go-stack/stack v1.8.0 // indirect github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.3.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.4 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.4 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
go.opentelemetry.io/otel v1.14.0 // indirect
go.opentelemetry.io/otel/trace v1.14.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
) )

186
go.sum
View File

@@ -1,51 +1,183 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY= github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So= github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 h1:oEZYEpZo28Wdx+5FZo4aU7JFXu0WG/4wJWese5reQSA=
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201/go.mod h1:Y9WZUHEb+mpra02CbQ/QczLUe6f0Dezxaw5DCJlJQGo=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A= github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk= github.com/getlantern/errors v1.0.1/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
github.com/getlantern/errors v1.0.3 h1:Ne4Ycj7NI1BtSyAfVeAT/DNoxz7/S2BUc3L2Ht1YSHE=
github.com/getlantern/errors v1.0.3/go.mod h1:m8C7H1qmouvsGpwQqk/6NUpIVMpfzUPn608aBZDYV04=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc= github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0= github.com/getlantern/golog v0.0.0-20230206140254-6d0a2e0f79af h1:cvD5qCZpH/Q32Ae0i1W1lRkVuM21czEZaJpTuRiJjc4=
github.com/getlantern/golog v0.0.0-20230206140254-6d0a2e0f79af/go.mod h1:+ZU1h+iOVqWReBpky6d5Y2WL0sF2Llxu+QcxJFs2+OU=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o= github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc= github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc h1:sue+aeVx7JF5v36H1HfvcGFImLpSD5goj8d+MitovDU=
github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc/go.mod h1:D9RWpXy/EFPYxiKUURo2TB8UBosbqkiLhttRrZYtvqM=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA= github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA= github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 h1:cSrD9ryDfTV2yaur9Qk3rHYD414j3Q1rl7+L0AylxrE=
github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770/go.mod h1:GOQsoDnEHl6ZmNIL+5uVo+JWRFWozMEp18Izcb++H+A=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA= github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
github.com/getlantern/ops v0.0.0-20220713155959-1315d978fff7 h1:Od0xvR4iK3gZwhkIbxnHw4Teusv+n5G/F9dW7x+C2f0=
github.com/getlantern/ops v0.0.0-20220713155959-1315d978fff7/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
github.com/getlantern/systray v1.2.1 h1:udsC2k98v2hN359VTFShuQW6GGprRprw6kD6539JikI= github.com/getlantern/systray v1.2.1 h1:udsC2k98v2hN359VTFShuQW6GGprRprw6kD6539JikI=
github.com/getlantern/systray v1.2.1/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM= github.com/getlantern/systray v1.2.1/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM=
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx/v2 v2.0.9 h1:TRX4Q630UXxPVLvP5vGaqVJO7S+0PE6msRZUsFSBoC8=
github.com/lestrrat-go/jwx/v2 v2.0.9/go.mod h1:K68euYaR95FnL0hIQB8VvzL70vB7pSifbJUydCTPmgM=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 h1:NvGWuYG8dkDHFSKksI1P9faiVJ9rayE6l0+ouWVIDs8= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo=
go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM=
go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU=
go.opentelemetry.io/otel/trace v1.9.0/go.mod h1:2737Q0MuG8q1uILYm2YYVkAyLtOofiTNGg6VODnOiPo=
go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M=
go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.3.3 h1:jXG9ANrwBc4+bMvBcSl8zCfPBaVoPyBEBshA8dA93X8= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gorm.io/driver/mysql v1.3.3/go.mod h1:ChK6AHbHgDCFZyJp0F+BmVGb06PSIoh9uVYKAlRbb2U= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0=
gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM= gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8=
gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU=
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
tawesoft.co.uk/go/dialog v0.0.0-20201103210221-4175697d086f h1:9LAzHynM3Jq3FZxP6rhoK3l96fSN+S47KABTOzE6gf4=
tawesoft.co.uk/go/dialog v0.0.0-20201103210221-4175697d086f/go.mod h1:h1pWmQQT/jWrxa6VT3M7lFUif/2F0+WVISAi447Punc=

73
main.go
View File

@@ -1,17 +1,82 @@
//go:build !windows //go:build !windows
// +build !windows
package main package main
import ( import (
"crypto/rsa"
"flag"
"fmt" "fmt"
"log"
"opensavecloudserver/config"
"opensavecloudserver/config/security"
"opensavecloudserver/constant" "opensavecloudserver/constant"
"opensavecloudserver/data/datasource/pgsql"
"opensavecloudserver/data/datasource/pgsql/models/game"
"opensavecloudserver/data/datasource/pgsql/models/user"
"opensavecloudserver/server" "opensavecloudserver/server"
"opensavecloudserver/server/authentication/impl"
"os"
"runtime" "runtime"
) )
func main() { func main() {
fmt.Printf("Open Save Cloud (Server) %s (%s %s)\n", constant.Version, runtime.GOOS, runtime.GOARCH) fmt.Printf("Open Save Cloud (Server) v%s %s/%s\n", constant.Version, runtime.GOOS, runtime.GOARCH)
InitCommon()
server.Serve() defer func() {
if err := recover(); err != nil {
_, err := fmt.Fprintln(os.Stderr, "the server has encountered an error and must stop: ", err)
if err != nil {
return
}
os.Exit(1)
}
}()
if err := initLogger(); err != nil {
panic(err)
}
path := flag.String("config", "./config.yml", "Set the configuration file path")
generateRSAKey := flag.Bool("new-rsa-key", false, "Generate a new RSA key. This key will be written to the rsa_key path set in the configuration file")
flag.Parse()
appConfiguration, err := config.Load(*path)
if err != nil {
log.Fatal(err)
}
var pk *rsa.PrivateKey
if *generateRSAKey {
pk, err = security.GenerateNewRSAKey(appConfiguration.Path.RSAKey, 2048)
if err != nil {
panic(err)
}
} else {
pk, err = security.LoadPrivateKey(appConfiguration.Path.RSAKey)
if err != nil {
panic(err)
}
}
dao := pgsql.NewDatabaseDatasource()
err = dao.Connect(appConfiguration.Database.URL)
if err != nil {
log.Fatal("failed to connect to the datasource: ", err)
}
userRepo := user.NewUserDatasource(dao)
jwtAuthenticator, err := impl.NewJWTAuthenticator(pk, userRepo)
if err != nil {
log.Fatal(err)
}
deps := server.DatasourceDependencies{
UserRepository: userRepo,
GameRepository: game.NewGameDatasource(dao),
Authenticator: jwtAuthenticator,
}
appServer := server.NewServer(appConfiguration, deps)
log.Println("the server is up and ready to listen on", appServer.Server.Addr)
err = appServer.Server.ListenAndServe()
if err != nil {
log.Fatal(err)
}
} }

View File

@@ -5,9 +5,10 @@ package main
import ( import (
_ "embed" _ "embed"
"github.com/getlantern/systray" "github.com/getlantern/systray"
"opensavecloudserver/constant" "log"
"opensavecloudserver/server" "net/http"
"os"
"tawesoft.co.uk/go/dialog"
) )
//go:generate go-winres make //go:generate go-winres make
@@ -16,32 +17,50 @@ import (
var icon []byte var icon []byte
func main() { func main() {
go func() { path := flag.String("config", "./config.yml", "Set the configuration file path")
InitCommon() flag.Parse()
server.Serve() appConfiguration, err := config.Load(*path)
}() if err != nil {
systray.Run(onReady, onExit) dialog.Alert("An error occured while starting the server: " + err.Error())
return
} }
func onReady() { if err := initLogger(); err != nil {
dialog.Alert("An error occured while starting the server: " + err.Error())
return
}
appServer := server.NewServer(appConfiguration)
go func(s *http.Server) {
err := s.ListenAndServe()
defer systray.Quit()
if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}(appServer.Server)
startWindowsApp(appServer.Server)
}
func startWindowsApp(appServer *http.Server) {
onReady := func() {
systray.SetIcon(icon) systray.SetIcon(icon)
systray.SetTitle("Open Save Cloud Server") systray.SetTitle("Open Save Cloud Server")
systray.SetTooltip("Open Save Cloud Server") systray.SetTooltip("The server is up and ready")
systray.AddMenuItem("Open Save Cloud", "").Disable() systray.AddMenuItem("Open Save Cloud "+constant.Version, "").Disable()
systray.AddMenuItem(constant.Version, "").Disable() systray.AddMenuItem("Running on "+appServer.Addr, "").Disable()
systray.AddSeparator() systray.AddSeparator()
mQuit := systray.AddMenuItem("Quit", "Quit the server") mQuit := systray.AddMenuItem("Shutdown", "Quit the server")
for {
select { select {
case <-mQuit.ClickedCh: case <-mQuit.ClickedCh:
quit() func(s *http.Server) {
mQuit.Disable()
systray.SetTooltip("Shutting down the server...")
s.Shutdown()
}(appServer)
} }
} }
func quit() {
systray.Quit()
os.Exit(0)
} }
onExit := func() {}
func onExit() { systray.Run(onReady, onExit)
systray.Quit()
} }

View File

@@ -1,220 +1,33 @@
package server package server
import ( import (
"encoding/json"
"github.com/go-chi/chi/v5"
"io"
"log"
"net/http" "net/http"
"opensavecloudserver/admin"
"opensavecloudserver/authentication"
"opensavecloudserver/database"
"strconv"
"time"
) )
type UpdateUsername struct { func (s *HTTPServer) createUserHandler(w http.ResponseWriter, r *http.Request) {
Id int `json:"id"` // TODO
Username string `json:"username"`
} }
func AddUser(w http.ResponseWriter, r *http.Request) { func (s *HTTPServer) deleteUserHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body) // TODO
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
userInfo := new(authentication.Registration)
err = json.Unmarshal(body, userInfo)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
err = authentication.Register(userInfo)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
user, err := database.UserByUsername(userInfo.Username)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
ok(user, w, r)
} }
func RemoveUser(w http.ResponseWriter, r *http.Request) { func (s *HTTPServer) listAllServerUsersHandler(w http.ResponseWriter, r *http.Request) {
queryId := chi.URLParam(r, "id") // TODO
id, err := strconv.Atoi(queryId)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
user, err := database.UserById(id)
if err != nil {
notFound(err.Error(), w, r)
log.Println(err)
return
}
err = admin.RemoveUser(user)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
ok(user, w, r)
} }
func AllUsers(w http.ResponseWriter, r *http.Request) { func (s *HTTPServer) userHandler(w http.ResponseWriter, r *http.Request) {
users, err := database.AllUsers() // TODO
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
ok(users, w, r)
} }
func User(w http.ResponseWriter, r *http.Request) { func (s *HTTPServer) updateUserRoleHandler(w http.ResponseWriter, r *http.Request) {
queryId := chi.URLParam(r, "id") // TODO
id, err := strconv.Atoi(queryId)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
user, err := database.UserById(id)
if err != nil {
notFound(err.Error(), w, r)
log.Println(err)
return
}
ok(user, w, r)
} }
func SetAdmin(w http.ResponseWriter, r *http.Request) { func (s *HTTPServer) updateUserPasswordHandler(w http.ResponseWriter, r *http.Request) {
queryId := chi.URLParam(r, "id") // TODO
id, err := strconv.Atoi(queryId)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
user, err := database.UserById(id)
if err != nil {
notFound(err.Error(), w, r)
log.Println(err)
return
}
err = admin.SetAdmin(user)
if err != nil {
notFound(err.Error(), w, r)
log.Println(err)
return
}
ok(user, w, r)
} }
func SetNotAdmin(w http.ResponseWriter, r *http.Request) { func (s *HTTPServer) updateUsernameHandler(w http.ResponseWriter, r *http.Request) {
queryId := chi.URLParam(r, "id") // TODO
id, err := strconv.Atoi(queryId)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
user, err := database.UserById(id)
if err != nil {
notFound(err.Error(), w, r)
log.Println(err)
return
}
err = admin.RemoveAdminRole(user)
if err != nil {
notFound(err.Error(), w, r)
log.Println(err)
return
}
ok(user, w, r)
}
func ChangeUserPassword(w http.ResponseWriter, r *http.Request) {
queryId := chi.URLParam(r, "id")
userId, err := strconv.Atoi(queryId)
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)
}
func ChangeUsername(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
newUserInfo := new(UpdateUsername)
err = json.Unmarshal(body, newUserInfo)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
if len(newUserInfo.Username) < 3 {
badRequest("username need at least 3 characters", w, r)
return
}
_, err = database.UserByUsername(newUserInfo.Username)
if err == nil {
badRequest("username already exist", w, r)
return
}
err = database.ChangeUsername(newUserInfo.Id, newUserInfo.Username)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
payload := &successMessage{
Message: "Username changed",
Timestamp: time.Now(),
Status: 200,
}
ok(payload, w, r)
} }

View File

@@ -3,94 +3,88 @@ package server
import ( import (
"encoding/json" "encoding/json"
"io" "io"
"log"
"net/http" "net/http"
"opensavecloudserver/authentication"
"time"
) )
type Credential struct { type (
userLogin struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
} }
type TokenValidation struct { userRegistration struct {
Valid bool `json:"valid"` UserUsername string `json:"username"`
UserPassword string `json:"password"`
UserDisplayName string `json:"displayName"`
} }
func Login(w http.ResponseWriter, r *http.Request) { userPresenter struct {
ID string `json:"id"`
Username string `json:"username"`
DisplayName string `json:"displayName"`
}
jwtPresenter struct {
Token []byte `json:"token"`
}
)
func (ur userRegistration) Username() string {
return ur.UserUsername
}
func (ur userRegistration) Password() string {
return ur.UserPassword
}
func (ur userRegistration) DisplayName() string {
return ur.UserDisplayName
}
func (s *HTTPServer) loginUserHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
internalServerError(w, r) panic(err)
log.Println(err)
return
} }
credential := new(Credential)
err = json.Unmarshal(body, credential) var ul userLogin
err = json.Unmarshal(body, &ul)
if err != nil { if err != nil {
internalServerError(w, r) panic(err)
log.Println(err)
return
} }
token, err := authentication.Connect(credential.Username, credential.Password)
jwt, err := s.deps.Authenticator.Authenticate(ul.Username, ul.Password)
if err != nil { if err != nil {
unauthorized(w, r) unauthorized(w, r)
return return
} }
ok(token, w, r)
payload := jwtPresenter{
Token: jwt,
}
ok(payload, w)
} }
func Register(w http.ResponseWriter, r *http.Request) { func (s *HTTPServer) registerUserHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
internalServerError(w, r) panic(err)
log.Println(err)
return
}
registration := new(authentication.Registration)
err = json.Unmarshal(body, registration)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
err = authentication.Register(registration)
if err != nil {
badRequest(err.Error(), w, r)
return
}
payload := successMessage{
Message: "You are now registered",
Timestamp: time.Now(),
Status: 200,
}
ok(payload, w, r)
} }
func CheckToken(w http.ResponseWriter, r *http.Request) { var ur userRegistration
body, err := io.ReadAll(r.Body) err = json.Unmarshal(body, &ur)
if err != nil { if err != nil {
internalServerError(w, r) panic(err)
log.Println(err)
return
} }
credential := new(authentication.AccessToken)
err = json.Unmarshal(body, credential) u, err := s.deps.UserRepository.CreateUser(ur)
if err != nil { if err != nil {
internalServerError(w, r) panic(err)
log.Println(err)
return
} }
_, err = authentication.ParseToken(credential.Token) payload := userPresenter{
if err != nil { ID: u.ID().String(),
payload := TokenValidation{ Username: u.Username(),
Valid: false, DisplayName: u.DisplayName(),
} }
ok(payload, w, r) ok(payload, w)
return
}
payload := TokenValidation{
Valid: true,
}
ok(payload, w, r)
} }

View File

@@ -0,0 +1,25 @@
package authentication
import (
"errors"
"opensavecloudserver/data/repository/user"
)
type (
Authenticator interface {
Authenticate(username, password string) ([]byte, error)
Validate(token string) (Session, error)
}
Session interface {
UserID() user.ID
Scopes() []Scope
Roles() []user.Role
}
Scope string
)
var (
ErrBadPassword = errors.New("failed to verify password")
)

View File

@@ -0,0 +1,128 @@
package impl
import (
"crypto/rsa"
"errors"
"github.com/google/uuid"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jwt"
"opensavecloudserver/data/repository/user"
"opensavecloudserver/server/authentication"
"time"
)
type (
JWTAuthenticator struct {
userRepo user.UserRepository
key jwk.Key
pubKey jwk.Key
}
JWTSession struct {
id user.ID
scopes []authentication.Scope
roles []user.Role
}
claimKey string
)
func (J JWTSession) UserID() user.ID {
return J.id
}
func (J JWTSession) Scopes() []authentication.Scope {
return J.scopes
}
func (J JWTSession) Roles() []user.Role {
return J.roles
}
const (
userScopesClaimKey claimKey = "user.scopes"
userRolesClaimKey claimKey = "user.roles"
issuerName string = "oscs"
)
var (
ErrScopesNotFound = errors.New("scopes not found in JWT")
ErrRolesNotFound = errors.New("roles not found in JWT")
)
func (J *JWTAuthenticator) Authenticate(username, password string) ([]byte, error) {
u, err := J.userRepo.UserByUsername(username)
if err != nil {
return nil, err
}
if u.CheckPassword(password) {
// Build a JWT!
tok, err := jwt.NewBuilder().
Issuer(issuerName).
IssuedAt(time.Now()).
Subject(u.ID().String()).
Claim(string(userScopesClaimKey), "").
Claim(string(userRolesClaimKey), u.Roles()).
Build()
if err != nil {
return nil, err
}
// Sign a JWT!
return jwt.Sign(tok, jwt.WithKey(jwa.RS256, J.key))
}
return nil, authentication.ErrBadPassword
}
func (J *JWTAuthenticator) Validate(token string) (authentication.Session, error) {
verifiedToken, err := jwt.Parse([]byte(token), jwt.WithKey(jwa.RS256, J.pubKey), jwt.WithValidate(true), jwt.WithIssuer(issuerName))
if err != nil {
return nil, err
}
id, err := uuid.Parse(verifiedToken.Subject())
if err != nil {
return nil, err
}
value, ok := verifiedToken.Get(string(userScopesClaimKey))
if !ok {
return nil, ErrScopesNotFound
}
scopes, ok := value.([]authentication.Scope)
if !ok {
return nil, ErrScopesNotFound
}
value, ok = verifiedToken.Get(string(userRolesClaimKey))
if !ok {
return nil, ErrRolesNotFound
}
roles, ok := value.([]user.Role)
if !ok {
return nil, ErrRolesNotFound
}
return JWTSession{
id: user.ID(id),
scopes: scopes,
roles: roles,
}, nil
}
func NewJWTAuthenticator(key *rsa.PrivateKey, userRepo user.UserRepository) (authentication.Authenticator, error) {
a := new(JWTAuthenticator)
a.userRepo = userRepo
// Parse, serialize, slice and dice JWKs!
privkey, err := jwk.FromRaw(key)
if err != nil {
return nil, err
}
pubkey, err := jwk.PublicKeyOf(privkey)
if err != nil {
return nil, err
}
a.key = privkey
a.pubKey = pubkey
return a, nil
}

View File

@@ -1,348 +1,42 @@
package server package server
import ( import (
"encoding/json"
"github.com/go-chi/chi/v5"
"io"
"log"
"mime/multipart"
"net/http" "net/http"
"opensavecloudserver/config"
"opensavecloudserver/database"
"opensavecloudserver/upload"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"unicode/utf8"
) )
type NewGameInfo struct { // createSaveEntryHandler create a game entry to the database
Name string `json:"name"` func (s *HTTPServer) createSaveEntryHandler(w http.ResponseWriter, r *http.Request) {
// TODO
} }
type UploadGameInfo struct { // saveInformationHandler get the game save information from the database
GameId int `json:"game_id"` func (s *HTTPServer) saveInformationHandler(w http.ResponseWriter, r *http.Request) {
// TODO
} }
type LockError struct { // allUserSavesInformationHandler all game saves information for a user
Message string `json:"message"` func (s *HTTPServer) allUserSavesInformationHandler(w http.ResponseWriter, r *http.Request) {
// TODO
} }
type NewPassword struct { // uploadDataHandler upload the game save archive to the storage folder
Password string `json:"password"` func (s *HTTPServer) uploadDataHandler(w http.ResponseWriter, r *http.Request) {
VerifyPassword string `json:"verify_password"` // TODO
} }
// CreateGame create a game entry to the database // downloadDataHandler send the game save archive to the client
func CreateGame(w http.ResponseWriter, r *http.Request) { func (s *HTTPServer) downloadDataHandler(w http.ResponseWriter, r *http.Request) {
userId, err := userIdFromContext(r.Context()) // TODO
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
}
gameInfo := new(NewGameInfo)
err = json.Unmarshal(body, gameInfo)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
game, err := database.CreateGame(userId, gameInfo.Name)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
ok(game, w, r)
} }
// GameInfoByID get the game save information from the database func (s *HTTPServer) currentUserHandler(w http.ResponseWriter, r *http.Request) {
func GameInfoByID(w http.ResponseWriter, r *http.Request) { // TODO
userId, err := userIdFromContext(r.Context())
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
queryId := chi.URLParam(r, "id")
id, err := strconv.Atoi(queryId)
if err != nil {
badRequest("Game ID missing or not an int", w, r)
log.Println(err)
return
}
game, err := database.GameInfoById(userId, id)
if err != nil {
notFound(err.Error(), w, r)
log.Println(err)
return
}
ok(game, w, r)
} }
// AllGamesInformation all game saves information for a user func (s *HTTPServer) updateCurrentUserPasswordHandler(w http.ResponseWriter, r *http.Request) {
func AllGamesInformation(w http.ResponseWriter, r *http.Request) { // TODO
userId, err := userIdFromContext(r.Context())
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
games, err := database.GameInfosByUserId(userId)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
ok(games, w, r)
} }
// AskForUpload check if the game save is not lock, then lock it and generate a token func (s *HTTPServer) deleteSave(w http.ResponseWriter, r *http.Request) {
func AskForUpload(w http.ResponseWriter, r *http.Request) { // TODO
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
}
gameInfo := new(UploadGameInfo)
err = json.Unmarshal(body, gameInfo)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
token, err := upload.AskForUpload(userId, gameInfo.GameId)
if err != nil {
ok(LockError{Message: err.Error()}, w, r)
return
}
ok(token, w, r)
}
// UploadSave upload the game save archive to the storage folder
func UploadSave(w http.ResponseWriter, r *http.Request) {
userId, err := userIdFromContext(r.Context())
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
gameId, err := gameIdFromContext(r.Context())
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
defer upload.UnlockGame(gameId)
hash := r.Header.Get("X-Game-Save-Hash")
if utf8.RuneCountInString(hash) == 0 {
badRequest("The header X-Game-Save-Hash is missing", w, r)
return
}
archiveHash := strings.ToLower(r.Header.Get("X-Hash"))
if utf8.RuneCountInString(hash) == 0 {
badRequest("The header X-Hash is missing", w, r)
return
}
game, err := database.GameInfoById(userId, gameId)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
file, _, err := r.FormFile("file")
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
defer func(file multipart.File) {
err := file.Close()
if err != nil {
log.Println(err)
}
}(file)
err = upload.UploadToCache(file, game)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
err = upload.ValidateAndMove(game, archiveHash)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
err = database.UpdateGameRevision(game, hash)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
payload := &successMessage{
Message: "Game uploaded",
Timestamp: time.Now(),
Status: 200,
}
ok(payload, w, r)
}
// Download send the game save archive to the client
func Download(w http.ResponseWriter, r *http.Request) {
userId, err := userIdFromContext(r.Context())
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
gameId, err := gameIdFromContext(r.Context())
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
defer upload.UnlockGame(gameId)
game, err := database.GameInfoById(userId, gameId)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
savePath := filepath.Join(config.Path().Storage, strconv.Itoa(userId), game.PathStorage)
if _, err := os.Stat(savePath); err == nil {
hash, err := upload.FileHash(savePath)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
file, err := os.Open(savePath)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
defer func(file *os.File) {
err := file.Close()
if err != nil {
log.Println(err)
}
}(file)
w.Header().Add("X-Hash", strings.ToUpper(hash))
_, err = io.Copy(w, file)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
} else {
http.NotFound(w, r)
}
}
func UserInformation(w http.ResponseWriter, r *http.Request) {
userId, err := userIdFromContext(r.Context())
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
user, err := database.UserById(userId)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
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)
}
func RemoveGame(w http.ResponseWriter, r *http.Request) {
userId, err := userIdFromContext(r.Context())
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
queryId := chi.URLParam(r, "id")
id, err := strconv.Atoi(queryId)
if err != nil {
badRequest("Game ID missing or not an int", w, r)
log.Println(err)
return
}
game, err := database.GameInfoById(userId, id)
if err != nil {
notFound(err.Error(), w, r)
log.Println(err)
return
}
err = upload.RemoveGame(userId, game)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
err = database.RemoveGame(game)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
ok(game, w, r)
} }

36
server/middleware.go Normal file
View File

@@ -0,0 +1,36 @@
package server
import "net/http"
// authMiddleware check the authentication token before accessing to the resource
func (s *HTTPServer) authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO
})
}
// adminMiddleware check the role of the user before accessing to the resource
func (s *HTTPServer) adminMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO
})
}
// uploadMiddleware check the upload key before allowing to upload a file
func (s *HTTPServer) uploadMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO
})
}
func recoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
err := recover()
if err != nil {
internalServerError(w, r)
}
}()
next.ServeHTTP(w, r)
})
}

View File

@@ -15,15 +15,9 @@ type httpError struct {
Path string `json:"path"` Path string `json:"path"`
} }
type successMessage struct {
Status int `json:"status"`
Timestamp time.Time `json:"timestamp"`
Message string `json:"message"`
}
func internalServerError(w http.ResponseWriter, r *http.Request) { func internalServerError(w http.ResponseWriter, r *http.Request) {
e := httpError{ e := httpError{
Status: 500, Status: http.StatusInternalServerError,
Error: "Internal Server Error", Error: "Internal Server Error",
Message: "The server encountered an unexpected condition that prevented it from fulfilling the request.", Message: "The server encountered an unexpected condition that prevented it from fulfilling the request.",
Path: r.RequestURI, Path: r.RequestURI,
@@ -35,7 +29,7 @@ func internalServerError(w http.ResponseWriter, r *http.Request) {
log.Println(err) log.Println(err)
} }
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
w.WriteHeader(500) w.WriteHeader(http.StatusInternalServerError)
_, err = w.Write(payload) _, err = w.Write(payload)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@@ -44,7 +38,7 @@ func internalServerError(w http.ResponseWriter, r *http.Request) {
func notFound(message string, w http.ResponseWriter, r *http.Request) { func notFound(message string, w http.ResponseWriter, r *http.Request) {
e := httpError{ e := httpError{
Status: 404, Status: http.StatusNotFound,
Error: "Not Found", Error: "Not Found",
Message: message, Message: message,
Path: r.RequestURI, Path: r.RequestURI,
@@ -56,7 +50,28 @@ func notFound(message string, w http.ResponseWriter, r *http.Request) {
log.Println(err) log.Println(err)
} }
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
w.WriteHeader(404) w.WriteHeader(http.StatusNotFound)
_, err = w.Write(payload)
if err != nil {
log.Println(err)
}
}
func methodNotAllowed(w http.ResponseWriter, r *http.Request) {
e := httpError{
Status: http.StatusMethodNotAllowed,
Error: "Method Not Allowed",
Message: "The server knows the request method, but the target resource doesn't support this method",
Path: r.RequestURI,
Timestamp: time.Now(),
}
payload, err := json.Marshal(e)
if err != nil {
log.Println(err)
}
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusMethodNotAllowed)
_, err = w.Write(payload) _, err = w.Write(payload)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@@ -65,7 +80,7 @@ func notFound(message string, w http.ResponseWriter, r *http.Request) {
func unauthorized(w http.ResponseWriter, r *http.Request) { func unauthorized(w http.ResponseWriter, r *http.Request) {
e := httpError{ e := httpError{
Status: 401, Status: http.StatusUnauthorized,
Error: "Unauthorized", Error: "Unauthorized",
Message: "The request has not been completed because it lacks valid authentication credentials for the requested resource.", Message: "The request has not been completed because it lacks valid authentication credentials for the requested resource.",
Path: r.RequestURI, Path: r.RequestURI,
@@ -77,8 +92,8 @@ func unauthorized(w http.ResponseWriter, r *http.Request) {
log.Println(err) log.Println(err)
} }
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
w.Header().Add("WWW-Authenticate", "Custom realm=\"Login via /api/login\"") w.Header().Add("WWW-Authenticate", "Custom realm=\"loginUserHandler via /api/login\"")
w.WriteHeader(401) w.WriteHeader(http.StatusUnauthorized)
_, err = w.Write(payload) _, err = w.Write(payload)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@@ -87,8 +102,8 @@ func unauthorized(w http.ResponseWriter, r *http.Request) {
func forbidden(w http.ResponseWriter, r *http.Request) { func forbidden(w http.ResponseWriter, r *http.Request) {
e := httpError{ e := httpError{
Status: 403, Status: http.StatusForbidden,
Error: "Unauthorized", Error: "Forbidden",
Message: "The access is permanently forbidden and tied to the application logic, such as insufficient rights to a resource.", Message: "The access is permanently forbidden and tied to the application logic, such as insufficient rights to a resource.",
Path: r.RequestURI, Path: r.RequestURI,
Timestamp: time.Now(), Timestamp: time.Now(),
@@ -99,14 +114,14 @@ func forbidden(w http.ResponseWriter, r *http.Request) {
log.Println(err) log.Println(err)
} }
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
w.WriteHeader(403) w.WriteHeader(http.StatusForbidden)
_, err = w.Write(payload) _, err = w.Write(payload)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
} }
} }
func ok(obj interface{}, w http.ResponseWriter, _ *http.Request) { func ok(obj interface{}, w http.ResponseWriter) {
payload, err := json.Marshal(obj) payload, err := json.Marshal(obj)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@@ -120,7 +135,7 @@ func ok(obj interface{}, w http.ResponseWriter, _ *http.Request) {
func badRequest(message string, w http.ResponseWriter, r *http.Request) { func badRequest(message string, w http.ResponseWriter, r *http.Request) {
e := httpError{ e := httpError{
Status: 400, Status: http.StatusBadRequest,
Error: "Bad Request", Error: "Bad Request",
Message: message, Message: message,
Path: r.RequestURI, Path: r.RequestURI,
@@ -132,7 +147,7 @@ func badRequest(message string, w http.ResponseWriter, r *http.Request) {
log.Println(err) log.Println(err)
} }
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
w.WriteHeader(400) w.WriteHeader(http.StatusBadRequest)
_, err = w.Write(payload) _, err = w.Write(payload)
if err != nil { if err != nil {
log.Println(err) log.Println(err)

View File

@@ -1,166 +1,138 @@
package server package server
import ( import (
"context" "encoding/json"
"errors"
"fmt" "fmt"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"log" "log"
"net/http" "net/http"
"opensavecloudserver/authentication"
"opensavecloudserver/config" "opensavecloudserver/config"
"opensavecloudserver/database" "opensavecloudserver/data/repository/game"
"opensavecloudserver/upload" "opensavecloudserver/data/repository/user"
"opensavecloudserver/server/authentication"
"time"
) )
type ContextKey string type (
HTTPServer struct {
Server *http.Server
config config.Configuration
deps DatasourceDependencies
}
const ( DatasourceDependencies struct {
UserIdKey ContextKey = "userId" UserRepository user.UserRepository
GameIdKey ContextKey = "gameId" GameRepository game.GameRepository
Authenticator authentication.Authenticator
}
) )
// Serve start the http server // NewServer start the http server
func Serve() { func NewServer(config config.Configuration, deps DatasourceDependencies) *HTTPServer {
s := &HTTPServer{config: config, deps: deps}
router := chi.NewRouter() router := chi.NewRouter()
router.NotFound(func(writer http.ResponseWriter, request *http.Request) {
notFound("This route does not exist", writer, request)
})
router.MethodNotAllowed(func(writer http.ResponseWriter, request *http.Request) {
methodNotAllowed(writer, request)
})
router.Use(middleware.Logger) router.Use(middleware.Logger)
router.Use(recovery) router.Use(recoverMiddleware)
router.Route("/api", func(rApi chi.Router) { if config.Server.Compress {
rApi.Route("/v1", func(r chi.Router) { router.Use(middleware.Compress(5, "application/json", "application/octetstream"))
r.Post("/login", Login)
r.Post("/check/token", CheckToken)
if config.Features().AllowRegister {
r.Post("/register", Register)
} }
r.Route("/system", func(systemRouter chi.Router) { if config.Server.PingEndPoint {
systemRouter.Get("/information", Information) router.Use(middleware.Heartbeat("/heartbeat"))
}) }
r.Route("/admin", func(adminRouter chi.Router) { router.Route("/api", func(routerAPI chi.Router) {
adminRouter.Use(adminMiddleware) routerAPI.Route("/v1", func(r chi.Router) {
adminRouter.Post("/user", AddUser) // Unsupported V1 because it was shitty, sorry about that
adminRouter.Post("/user/username", ChangeUsername) r.HandleFunc("/*", unsupportedAPIHandler)
adminRouter.Post("/user/passwd/{id}", ChangeUserPassword)
adminRouter.Delete("/user/{id}", RemoveUser)
adminRouter.Get("/user/{id}", User)
adminRouter.Get("/users", AllUsers)
adminRouter.Get("/user/role/admin/{id}", SetAdmin)
adminRouter.Get("/user/role/user/{id}", SetNotAdmin)
}) })
routerAPI.Route("/v2", func(r chi.Router) {
// Get information about the server
r.Get("/version", s.Information)
// Authentication routes
// Login and get a token
r.Post("/login", s.loginUserHandler)
if config.Features.AllowRegister {
// Register a user
r.Post("/register", s.registerUserHandler)
}
// Secured routes
r.Group(func(secureRouter chi.Router) { r.Group(func(secureRouter chi.Router) {
secureRouter.Use(authMiddleware) secureRouter.Use(s.authMiddleware)
// Logged user routes
secureRouter.Route("/user", func(userRouter chi.Router) { secureRouter.Route("/user", func(userRouter chi.Router) {
userRouter.Get("/information", UserInformation) // Get information about the logged user
userRouter.Post("/passwd", ChangePassword) userRouter.Get("/", s.currentUserHandler)
// Change the password of the current user
userRouter.Put("/password/update", s.updateCurrentUserPasswordHandler)
}) })
secureRouter.Route("/game", func(gameRouter chi.Router) { // Save files routes
gameRouter.Post("/create", CreateGame) secureRouter.Route("/saves", func(gameRouter chi.Router) {
gameRouter.Get("/all", AllGamesInformation) // Create a save entry
gameRouter.Delete("/remove/{id}", RemoveGame) gameRouter.Post("/create", s.createSaveEntryHandler)
gameRouter.Get("/info/{id}", GameInfoByID) // List all available saves
gameRouter.Post("/upload/init", AskForUpload) gameRouter.Get("/", s.allUserSavesInformationHandler)
// Remove a save
gameRouter.Delete("/{id}", s.deleteSave)
// Get the information about a save
gameRouter.Get("/{id}", s.saveInformationHandler)
// Data routes
gameRouter.Group(func(uploadRouter chi.Router) { gameRouter.Group(func(uploadRouter chi.Router) {
uploadRouter.Use(uploadMiddleware) uploadRouter.Use(s.uploadMiddleware)
uploadRouter.Post("/upload", UploadSave) // Upload data
uploadRouter.Get("/download", Download) uploadRouter.Put("/{id}/data", s.uploadDataHandler)
// downloadDataHandler data
uploadRouter.Get("/{id}/data", s.downloadDataHandler)
})
})
secureRouter.Route("/admin", func(adminRouter chi.Router) {
adminRouter.Use(s.adminMiddleware)
// Create a user
adminRouter.Post("/user/create", s.createUserHandler)
// Update the username of a user
adminRouter.Post("/user/{id}/username", s.updateUsernameHandler)
// Update the password of a user
adminRouter.Post("/user/{id}/password/update", s.updateUserPasswordHandler)
// Remove a user
adminRouter.Delete("/user/{id}", s.deleteUserHandler)
// Get information about a user
adminRouter.Get("/user/{id}", s.userHandler)
// List all user registered on the server
adminRouter.Get("/users", s.listAllServerUsersHandler)
// Update role
adminRouter.Put("/user/{id}/role", s.updateUserRoleHandler)
}) })
}) })
}) })
}) })
}) s.Server = &http.Server{
log.Println("Server is listening...") Addr: fmt.Sprintf(":%d", config.Server.Port),
err := http.ListenAndServe(fmt.Sprintf(":%d", config.Server().Port), router) Handler: router,
if err != nil {
log.Fatal(err)
} }
return s
} }
// authMiddleware check the authentication token before accessing to the resource func unsupportedAPIHandler(w http.ResponseWriter, r *http.Request) {
func authMiddleware(next http.Handler) http.Handler { e := httpError{
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { Status: http.StatusGone,
header := r.Header.Get("Authorization") Error: "API Not supported anymore",
if len(header) > 7 { Message: "This version of the server does not support the V1 version of the API.",
userId, err := authentication.ParseToken(header[7:]) Path: r.RequestURI,
Timestamp: time.Now(),
}
payload, err := json.Marshal(e)
if err != nil { if err != nil {
unauthorized(w, r)
return
}
ctx := context.WithValue(r.Context(), UserIdKey, userId)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
return
}
unauthorized(w, r)
})
}
// adminMiddleware check the role of the user before accessing to the resource
func adminMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
header := r.Header.Get("Authorization")
if len(header) > 7 {
userId, err := authentication.ParseToken(header[7:])
if err != nil {
unauthorized(w, r)
return
}
user, err := database.UserById(userId)
if err != nil {
internalServerError(w, r)
log.Println(err) log.Println(err)
return
} }
if !user.IsAdmin { w.Header().Add("Content-Type", "application/json")
forbidden(w, r) w.WriteHeader(http.StatusGone)
return _, err = w.Write(payload)
}
ctx := context.WithValue(r.Context(), UserIdKey, userId)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
return
}
unauthorized(w, r)
})
}
// uploadMiddleware check the upload key before allowing to upload a file
func uploadMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
header := r.Header.Get("X-Upload-Key")
if len(header) > 0 {
if gameId, ok := upload.CheckUploadToken(header); ok {
ctx := context.WithValue(r.Context(), GameIdKey, gameId)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
return
}
}
unauthorized(w, r)
})
}
func userIdFromContext(ctx context.Context) (int, error) {
if userId, ok := ctx.Value(UserIdKey).(int); ok {
return userId, nil
}
return 0, errors.New("userId not found in context")
}
func gameIdFromContext(ctx context.Context) (int, error) {
if gameId, ok := ctx.Value(GameIdKey).(int); ok {
return gameId, nil
}
return 0, errors.New("gameId not found in context")
}
func recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
err := recover()
if err != nil { if err != nil {
internalServerError(w, r) log.Println(err)
} }
}()
next.ServeHTTP(w, r)
})
} }

View File

@@ -2,7 +2,6 @@ package server
import ( import (
"net/http" "net/http"
"opensavecloudserver/config"
"opensavecloudserver/constant" "opensavecloudserver/constant"
"runtime" "runtime"
) )
@@ -10,20 +9,20 @@ import (
type information struct { type information struct {
AllowRegister bool `json:"allow_register"` AllowRegister bool `json:"allow_register"`
Version string `json:"version"` Version string `json:"version"`
ApiVersion int `json:"api_version"` APIVersion int `json:"api_version"`
GoVersion string `json:"go_version"` GoVersion string `json:"go_version"`
OsName string `json:"os_name"` OSName string `json:"os_name"`
OsArchitecture string `json:"os_architecture"` OSArchitecture string `json:"os_architecture"`
} }
func Information(w http.ResponseWriter, r *http.Request) { func (s *HTTPServer) Information(w http.ResponseWriter, r *http.Request) {
info := information{ info := information{
AllowRegister: config.Features().AllowRegister, AllowRegister: s.config.Features.AllowRegister,
Version: constant.Version, Version: constant.Version,
ApiVersion: constant.ApiVersion, APIVersion: constant.ApiVersion,
GoVersion: runtime.Version(), GoVersion: runtime.Version(),
OsName: runtime.GOOS, OSName: runtime.GOOS,
OsArchitecture: runtime.GOARCH, OSArchitecture: runtime.GOARCH,
} }
ok(info, w, r) ok(info, w)
} }

View File

@@ -9,6 +9,7 @@ import (
"log" "log"
"mime/multipart" "mime/multipart"
"opensavecloudserver/config" "opensavecloudserver/config"
database2 "opensavecloudserver/data/internal/database"
"opensavecloudserver/database" "opensavecloudserver/database"
"os" "os"
"path" "path"
@@ -108,7 +109,7 @@ func CheckUploadToken(uploadToken string) (int, bool) {
return -1, false return -1, false
} }
func UploadToCache(file multipart.File, game *database.Game) error { func UploadToCache(file multipart.File, game *database2.Game) error {
filePath := path.Join(config.Path().Cache, strconv.Itoa(game.UserId)) filePath := path.Join(config.Path().Cache, strconv.Itoa(game.UserId))
if _, err := os.Stat(filePath); err != nil { if _, err := os.Stat(filePath); err != nil {
err = os.Mkdir(filePath, 0766) err = os.Mkdir(filePath, 0766)
@@ -133,7 +134,7 @@ func UploadToCache(file multipart.File, game *database.Game) error {
return nil return nil
} }
func ValidateAndMove(game *database.Game, hash string) error { func ValidateAndMove(game *database2.Game, hash string) error {
filePath := path.Join(config.Path().Cache, strconv.Itoa(game.UserId), game.PathStorage) filePath := path.Join(config.Path().Cache, strconv.Itoa(game.UserId), game.PathStorage)
if err := checkHash(filePath, hash); err != nil { if err := checkHash(filePath, hash); err != nil {
return err return err
@@ -155,7 +156,7 @@ func checkHash(path, hash string) error {
return nil return nil
} }
func moveToStorage(cachePath string, game *database.Game) error { func moveToStorage(cachePath string, game *database2.Game) error {
filePath := path.Join(config.Path().Storage, strconv.Itoa(game.UserId)) filePath := path.Join(config.Path().Storage, strconv.Itoa(game.UserId))
if _, err := os.Stat(filePath); err != nil { if _, err := os.Stat(filePath); err != nil {
err = os.Mkdir(filePath, 0766) err = os.Mkdir(filePath, 0766)
@@ -200,7 +201,7 @@ func RemoveFolders(userId int) error {
return nil return nil
} }
func RemoveGame(userId int, game *database.Game) error { func RemoveGame(userId int, game *database2.Game) error {
filePath := path.Join(config.Path().Storage, strconv.Itoa(userId), game.PathStorage) filePath := path.Join(config.Path().Storage, strconv.Itoa(userId), game.PathStorage)
return os.Remove(filePath) return os.Remove(filePath)
} }