first commit

This commit is contained in:
Aurélie Delhaie
2022-05-08 14:08:27 +02:00
commit b20a53cc48
19 changed files with 611 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
config.yml
cache/
opensavecloudserver

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SwUserDefinedSpecifications">
<option name="specTypeByUrl">
<map />
</option>
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/opensavecloudserver.iml" filepath="$PROJECT_DIR$/.idea/opensavecloudserver.iml" />
</modules>
</component>
</project>

9
.idea/opensavecloudserver.iml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -0,0 +1,84 @@
package authentication
import (
"crypto/rand"
"errors"
"github.com/golang-jwt/jwt"
"golang.org/x/crypto/bcrypt"
"log"
"opensavecloudserver/database"
"time"
)
var secret []byte
type AccessToken struct {
Token string `json:"token"`
}
type Registration struct {
Username string `json:"username"`
Password string `json:"password"`
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
Pronouns int `json:"pronouns"`
}
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, user.Firstname, user.Lastname, hash, user.Pronouns)
}
func token(userId int) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{
"sub": userId,
"exp": time.Now().Add(1 * time.Hour).Unix(),
})
return token.SignedString(secret)
}

9
config.default.yml Normal file
View File

@@ -0,0 +1,9 @@
---
database:
host: localhost
password: root
port: 3306
username: root
features:
allow_register: false
cache: "/var/osc/cache"

52
config/config.go Normal file
View File

@@ -0,0 +1,52 @@
package config
import (
"flag"
"gopkg.in/yaml.v3"
"log"
"os"
)
type Configuration struct {
Database DatabaseConfiguration `yaml:"database"`
Features FeaturesConfiguration `yaml:"features"`
Cache string `yaml:"cache"`
}
type DatabaseConfiguration struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Username string `yaml:"username"`
Password string `yaml:"password"`
}
type FeaturesConfiguration struct {
AllowRegister bool `yaml:"allow_register"`
}
var currentConfig *Configuration
func init() {
path := flag.String("config", "./config.yml", "Set the configuration file path")
flag.Parse()
configYamlContent, err := os.ReadFile(*path)
if err != nil {
log.Fatal(err)
}
err = yaml.Unmarshal(configYamlContent, &currentConfig)
if err != nil {
log.Fatalf("error: %s", err)
}
}
func Database() *DatabaseConfiguration {
return &currentConfig.Database
}
func Features() *FeaturesConfiguration {
return &currentConfig.Features
}
func Cache() string {
return currentConfig.Cache
}

5
constant/constant.go Normal file
View File

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

83
database/database.go Normal file
View File

@@ -0,0 +1,83 @@
package database
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"log"
"opensavecloudserver/config"
"os"
"time"
)
var db *gorm.DB
func init() {
dbConfig := config.Database()
var err error
db, err = gorm.Open(mysql.Open(
fmt.Sprintf("%s:%s@tcp(%s:%d)/transagenda?charset=utf8mb4&parseTime=True&loc=Local",
dbConfig.Username,
dbConfig.Password,
dbConfig.Host,
dbConfig.Port),
), &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 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
}
return user, nil
}
func UserById(userId int) (*User, error) {
var user *User
err := db.Model(User{}).Where(User{ID: userId}).First(&user).Error
if err != nil {
return nil, err
}
return user, nil
}
func AddUser(username string, password []byte) error {
user := &User{
Username: username,
Password: password,
}
return db.Save(user).Error
}
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
}
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
}

20
database/model.go Normal file
View File

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

18
go.mod Normal file
View File

@@ -0,0 +1,18 @@
module opensavecloudserver
go 1.18
require (
github.com/go-chi/chi/v5 v5.0.7
github.com/golang-jwt/jwt v3.2.2+incompatible
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
gorm.io/driver/mysql v1.3.3
gorm.io/gorm v1.23.5
)
require (
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.4 // indirect
)

21
go.sum Normal file
View File

@@ -0,0 +1,21 @@
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
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/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 h1:NvGWuYG8dkDHFSKksI1P9faiVJ9rayE6l0+ouWVIDs8=
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
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/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.3.3 h1:jXG9ANrwBc4+bMvBcSl8zCfPBaVoPyBEBshA8dA93X8=
gorm.io/driver/mysql v1.3.3/go.mod h1:ChK6AHbHgDCFZyJp0F+BmVGb06PSIoh9uVYKAlRbb2U=
gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM=
gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=

13
main.go Normal file
View File

@@ -0,0 +1,13 @@
package main
import (
"fmt"
"opensavecloudserver/constant"
"opensavecloudserver/server"
"runtime"
)
func main() {
fmt.Printf("Open Save Cloud (Server) %s (%s %s)\n", constant.Version, runtime.GOOS, runtime.GOARCH)
server.Serve()
}

64
server/authentication.go Normal file
View File

@@ -0,0 +1,64 @@
package server
import (
"encoding/json"
"io"
"log"
"net/http"
"opensavecloudserver/authentication"
"time"
)
type Credential struct {
Username string `json:"username"`
Password string `json:"password"`
}
func Login(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
credential := new(Credential)
err = json.Unmarshal(body, credential)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
token, err := authentication.Connect(credential.Username, credential.Password)
if err != nil {
unauthorized(w, r)
return
}
ok(token, w, r)
}
func Register(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
internalServerError(w, r)
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)
}

1
server/data.go Normal file
View File

@@ -0,0 +1 @@
package server

98
server/response.go Normal file
View File

@@ -0,0 +1,98 @@
package server
import (
"encoding/json"
"log"
"net/http"
"time"
)
type httpError struct {
Status int `json:"status"`
Timestamp time.Time `json:"timestamp"`
Error string `json:"error"`
Message string `json:"message"`
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) {
e := httpError{
Status: 500,
Error: "Internal Server Error",
Message: "The server encountered an unexpected condition that prevented it from fulfilling the request.",
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(500)
_, err = w.Write(payload)
if err != nil {
log.Println(err)
}
}
func unauthorized(w http.ResponseWriter, r *http.Request) {
e := httpError{
Status: 401,
Error: "Unauthorized",
Message: "The request has not been completed because it lacks valid authentication credentials for the requested resource.",
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.Header().Add("WWW-Authenticate", "Custom realm=\"Login via /api/login\"")
w.WriteHeader(401)
_, err = w.Write(payload)
if err != nil {
log.Println(err)
}
}
func ok(obj interface{}, w http.ResponseWriter, _ *http.Request) {
payload, err := json.Marshal(obj)
if err != nil {
log.Println(err)
}
w.Header().Add("Content-Type", "application/json")
_, err = w.Write(payload)
if err != nil {
log.Println(err)
}
}
func badRequest(message string, w http.ResponseWriter, r *http.Request) {
e := httpError{
Status: 400,
Error: "Bad Request",
Message: message,
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(400)
_, err = w.Write(payload)
if err != nil {
log.Println(err)
}
}

78
server/server.go Normal file
View File

@@ -0,0 +1,78 @@
package server
import (
"context"
"errors"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"log"
"net/http"
"opensavecloudserver/authentication"
"opensavecloudserver/config"
)
type ContextKey string
const UserIdKey ContextKey = "userId"
// Serve start the http server
func Serve() {
router := chi.NewRouter()
router.Use(middleware.Logger)
router.Use(recovery)
router.Route("/api", func(r chi.Router) {
r.Post("/login", Login)
if config.Features().AllowRegister {
r.Post("/register", Register)
}
r.Route("/system", func(systemRouter chi.Router) {
systemRouter.Get("/information", Information)
})
r.Group(func(secureRouter chi.Router) {
secureRouter.Use(authMiddleware)
})
})
log.Println("Server is listening...")
err := http.ListenAndServe(":8080", router)
if err != nil {
log.Fatal(err)
}
}
// authMiddleware filter the request
func authMiddleware(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
}
ctx := context.WithValue(r.Context(), UserIdKey, userId)
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 recovery(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)
})
}

29
server/system.go Normal file
View File

@@ -0,0 +1,29 @@
package server
import (
"net/http"
"opensavecloudserver/config"
"opensavecloudserver/constant"
"runtime"
)
type information struct {
AllowRegister bool `json:"allow_register"`
Version string `json:"version"`
ApiVersion int `json:"api_version"`
GoVersion string `json:"go_version"`
OsName string `json:"os_name"`
OsArchitecture string `json:"os_architecture"`
}
func Information(w http.ResponseWriter, r *http.Request) {
info := information{
AllowRegister: config.Features().AllowRegister,
Version: constant.Version,
ApiVersion: constant.ApiVersion,
GoVersion: runtime.Version(),
OsName: runtime.GOOS,
OsArchitecture: runtime.GOARCH,
}
ok(info, w, r)
}