2 Commits

Author SHA1 Message Date
Aurélie Delhaie
c06843cd28 Start refactoring 2023-05-29 17:44:50 +02:00
Aurélie Delhaie
55ac50f3be License 2022-08-01 22:26:38 +02:00
32 changed files with 1132 additions and 1267 deletions

1
.gitignore vendored
View File

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

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright 2022 Aurélie Delhaie
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

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)
} }