Start refactoring

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

View File

@@ -1,220 +1,33 @@
package server
import (
"encoding/json"
"github.com/go-chi/chi/v5"
"io"
"log"
"net/http"
"opensavecloudserver/admin"
"opensavecloudserver/authentication"
"opensavecloudserver/database"
"strconv"
"time"
)
type UpdateUsername struct {
Id int `json:"id"`
Username string `json:"username"`
func (s *HTTPServer) createUserHandler(w http.ResponseWriter, r *http.Request) {
// TODO
}
func AddUser(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
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 (s *HTTPServer) deleteUserHandler(w http.ResponseWriter, r *http.Request) {
// TODO
}
func RemoveUser(w http.ResponseWriter, r *http.Request) {
queryId := chi.URLParam(r, "id")
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 (s *HTTPServer) listAllServerUsersHandler(w http.ResponseWriter, r *http.Request) {
// TODO
}
func AllUsers(w http.ResponseWriter, r *http.Request) {
users, err := database.AllUsers()
if err != nil {
internalServerError(w, r)
log.Println(err)
return
}
ok(users, w, r)
func (s *HTTPServer) userHandler(w http.ResponseWriter, r *http.Request) {
// TODO
}
func User(w http.ResponseWriter, r *http.Request) {
queryId := chi.URLParam(r, "id")
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 (s *HTTPServer) updateUserRoleHandler(w http.ResponseWriter, r *http.Request) {
// TODO
}
func SetAdmin(w http.ResponseWriter, r *http.Request) {
queryId := chi.URLParam(r, "id")
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 (s *HTTPServer) updateUserPasswordHandler(w http.ResponseWriter, r *http.Request) {
// TODO
}
func SetNotAdmin(w http.ResponseWriter, r *http.Request) {
queryId := chi.URLParam(r, "id")
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)
func (s *HTTPServer) updateUsernameHandler(w http.ResponseWriter, r *http.Request) {
// TODO
}

View File

@@ -3,94 +3,88 @@ package server
import (
"encoding/json"
"io"
"log"
"net/http"
"opensavecloudserver/authentication"
"time"
)
type Credential struct {
Username string `json:"username"`
Password string `json:"password"`
type (
userLogin struct {
Username string `json:"username"`
Password string `json:"password"`
}
userRegistration struct {
UserUsername string `json:"username"`
UserPassword string `json:"password"`
UserDisplayName string `json:"displayName"`
}
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
}
type TokenValidation struct {
Valid bool `json:"valid"`
func (ur userRegistration) Password() string {
return ur.UserPassword
}
func Login(w http.ResponseWriter, r *http.Request) {
func (ur userRegistration) DisplayName() string {
return ur.UserDisplayName
}
func (s *HTTPServer) loginUserHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
panic(err)
}
credential := new(Credential)
err = json.Unmarshal(body, credential)
var ul userLogin
err = json.Unmarshal(body, &ul)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
panic(err)
}
token, err := authentication.Connect(credential.Username, credential.Password)
jwt, err := s.deps.Authenticator.Authenticate(ul.Username, ul.Password)
if err != nil {
unauthorized(w, r)
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)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
panic(err)
}
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) {
body, err := io.ReadAll(r.Body)
var ur userRegistration
err = json.Unmarshal(body, &ur)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
panic(err)
}
credential := new(authentication.AccessToken)
err = json.Unmarshal(body, credential)
u, err := s.deps.UserRepository.CreateUser(ur)
if err != nil {
internalServerError(w, r)
log.Println(err)
return
panic(err)
}
_, err = authentication.ParseToken(credential.Token)
if err != nil {
payload := TokenValidation{
Valid: false,
}
ok(payload, w, r)
return
payload := userPresenter{
ID: u.ID().String(),
Username: u.Username(),
DisplayName: u.DisplayName(),
}
payload := TokenValidation{
Valid: true,
}
ok(payload, w, r)
ok(payload, w)
}

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
import (
"encoding/json"
"github.com/go-chi/chi/v5"
"io"
"log"
"mime/multipart"
"net/http"
"opensavecloudserver/config"
"opensavecloudserver/database"
"opensavecloudserver/upload"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"unicode/utf8"
)
type NewGameInfo struct {
Name string `json:"name"`
// createSaveEntryHandler create a game entry to the database
func (s *HTTPServer) createSaveEntryHandler(w http.ResponseWriter, r *http.Request) {
// TODO
}
type UploadGameInfo struct {
GameId int `json:"game_id"`
// saveInformationHandler get the game save information from the database
func (s *HTTPServer) saveInformationHandler(w http.ResponseWriter, r *http.Request) {
// TODO
}
type LockError struct {
Message string `json:"message"`
// allUserSavesInformationHandler all game saves information for a user
func (s *HTTPServer) allUserSavesInformationHandler(w http.ResponseWriter, r *http.Request) {
// TODO
}
type NewPassword struct {
Password string `json:"password"`
VerifyPassword string `json:"verify_password"`
// uploadDataHandler upload the game save archive to the storage folder
func (s *HTTPServer) uploadDataHandler(w http.ResponseWriter, r *http.Request) {
// TODO
}
// CreateGame create a game entry to the database
func CreateGame(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
}
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)
// downloadDataHandler send the game save archive to the client
func (s *HTTPServer) downloadDataHandler(w http.ResponseWriter, r *http.Request) {
// TODO
}
// GameInfoByID get the game save information from the database
func GameInfoByID(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
}
ok(game, w, r)
func (s *HTTPServer) currentUserHandler(w http.ResponseWriter, r *http.Request) {
// TODO
}
// AllGamesInformation all game saves information for a user
func AllGamesInformation(w http.ResponseWriter, r *http.Request) {
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)
func (s *HTTPServer) updateCurrentUserPasswordHandler(w http.ResponseWriter, r *http.Request) {
// TODO
}
// AskForUpload check if the game save is not lock, then lock it and generate a token
func AskForUpload(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
}
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)
func (s *HTTPServer) deleteSave(w http.ResponseWriter, r *http.Request) {
// TODO
}

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"`
}
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,
Status: http.StatusInternalServerError,
Error: "Internal Server Error",
Message: "The server encountered an unexpected condition that prevented it from fulfilling the request.",
Path: r.RequestURI,
@@ -35,7 +29,7 @@ func internalServerError(w http.ResponseWriter, r *http.Request) {
log.Println(err)
}
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(500)
w.WriteHeader(http.StatusInternalServerError)
_, err = w.Write(payload)
if err != nil {
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) {
e := httpError{
Status: 404,
Status: http.StatusNotFound,
Error: "Not Found",
Message: message,
Path: r.RequestURI,
@@ -56,7 +50,28 @@ func notFound(message string, w http.ResponseWriter, r *http.Request) {
log.Println(err)
}
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)
if err != nil {
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) {
e := httpError{
Status: 401,
Status: http.StatusUnauthorized,
Error: "Unauthorized",
Message: "The request has not been completed because it lacks valid authentication credentials for the requested resource.",
Path: r.RequestURI,
@@ -77,8 +92,8 @@ func unauthorized(w http.ResponseWriter, r *http.Request) {
log.Println(err)
}
w.Header().Add("Content-Type", "application/json")
w.Header().Add("WWW-Authenticate", "Custom realm=\"Login via /api/login\"")
w.WriteHeader(401)
w.Header().Add("WWW-Authenticate", "Custom realm=\"loginUserHandler via /api/login\"")
w.WriteHeader(http.StatusUnauthorized)
_, err = w.Write(payload)
if err != nil {
log.Println(err)
@@ -87,8 +102,8 @@ func unauthorized(w http.ResponseWriter, r *http.Request) {
func forbidden(w http.ResponseWriter, r *http.Request) {
e := httpError{
Status: 403,
Error: "Unauthorized",
Status: http.StatusForbidden,
Error: "Forbidden",
Message: "The access is permanently forbidden and tied to the application logic, such as insufficient rights to a resource.",
Path: r.RequestURI,
Timestamp: time.Now(),
@@ -99,14 +114,14 @@ func forbidden(w http.ResponseWriter, r *http.Request) {
log.Println(err)
}
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(403)
w.WriteHeader(http.StatusForbidden)
_, err = w.Write(payload)
if err != nil {
log.Println(err)
}
}
func ok(obj interface{}, w http.ResponseWriter, _ *http.Request) {
func ok(obj interface{}, w http.ResponseWriter) {
payload, err := json.Marshal(obj)
if err != nil {
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) {
e := httpError{
Status: 400,
Status: http.StatusBadRequest,
Error: "Bad Request",
Message: message,
Path: r.RequestURI,
@@ -132,7 +147,7 @@ func badRequest(message string, w http.ResponseWriter, r *http.Request) {
log.Println(err)
}
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(400)
w.WriteHeader(http.StatusBadRequest)
_, err = w.Write(payload)
if err != nil {
log.Println(err)

View File

@@ -1,166 +1,138 @@
package server
import (
"context"
"errors"
"encoding/json"
"fmt"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"log"
"net/http"
"opensavecloudserver/authentication"
"opensavecloudserver/config"
"opensavecloudserver/database"
"opensavecloudserver/upload"
"opensavecloudserver/data/repository/game"
"opensavecloudserver/data/repository/user"
"opensavecloudserver/server/authentication"
"time"
)
type ContextKey string
type (
HTTPServer struct {
Server *http.Server
config config.Configuration
deps DatasourceDependencies
}
const (
UserIdKey ContextKey = "userId"
GameIdKey ContextKey = "gameId"
DatasourceDependencies struct {
UserRepository user.UserRepository
GameRepository game.GameRepository
Authenticator authentication.Authenticator
}
)
// Serve start the http server
func Serve() {
// NewServer start the http server
func NewServer(config config.Configuration, deps DatasourceDependencies) *HTTPServer {
s := &HTTPServer{config: config, deps: deps}
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(recovery)
router.Route("/api", func(rApi chi.Router) {
rApi.Route("/v1", func(r chi.Router) {
r.Post("/login", Login)
r.Post("/check/token", CheckToken)
if config.Features().AllowRegister {
r.Post("/register", Register)
router.Use(recoverMiddleware)
if config.Server.Compress {
router.Use(middleware.Compress(5, "application/json", "application/octetstream"))
}
if config.Server.PingEndPoint {
router.Use(middleware.Heartbeat("/heartbeat"))
}
router.Route("/api", func(routerAPI chi.Router) {
routerAPI.Route("/v1", func(r chi.Router) {
// Unsupported V1 because it was shitty, sorry about that
r.HandleFunc("/*", unsupportedAPIHandler)
})
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)
}
r.Route("/system", func(systemRouter chi.Router) {
systemRouter.Get("/information", Information)
})
r.Route("/admin", func(adminRouter chi.Router) {
adminRouter.Use(adminMiddleware)
adminRouter.Post("/user", AddUser)
adminRouter.Post("/user/username", ChangeUsername)
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)
})
// Secured routes
r.Group(func(secureRouter chi.Router) {
secureRouter.Use(authMiddleware)
secureRouter.Use(s.authMiddleware)
// Logged user routes
secureRouter.Route("/user", func(userRouter chi.Router) {
userRouter.Get("/information", UserInformation)
userRouter.Post("/passwd", ChangePassword)
// Get information about the logged user
userRouter.Get("/", s.currentUserHandler)
// Change the password of the current user
userRouter.Put("/password/update", s.updateCurrentUserPasswordHandler)
})
secureRouter.Route("/game", func(gameRouter chi.Router) {
gameRouter.Post("/create", CreateGame)
gameRouter.Get("/all", AllGamesInformation)
gameRouter.Delete("/remove/{id}", RemoveGame)
gameRouter.Get("/info/{id}", GameInfoByID)
gameRouter.Post("/upload/init", AskForUpload)
// Save files routes
secureRouter.Route("/saves", func(gameRouter chi.Router) {
// Create a save entry
gameRouter.Post("/create", s.createSaveEntryHandler)
// List all available saves
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) {
uploadRouter.Use(uploadMiddleware)
uploadRouter.Post("/upload", UploadSave)
uploadRouter.Get("/download", Download)
uploadRouter.Use(s.uploadMiddleware)
// Upload data
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)
})
})
})
})
log.Println("Server is listening...")
err := http.ListenAndServe(fmt.Sprintf(":%d", config.Server().Port), router)
s.Server = &http.Server{
Addr: fmt.Sprintf(":%d", config.Server.Port),
Handler: router,
}
return s
}
func unsupportedAPIHandler(w http.ResponseWriter, r *http.Request) {
e := httpError{
Status: http.StatusGone,
Error: "API Not supported anymore",
Message: "This version of the server does not support the V1 version of the API.",
Path: r.RequestURI,
Timestamp: time.Now(),
}
payload, err := json.Marshal(e)
if err != nil {
log.Fatal(err)
log.Println(err)
}
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusGone)
_, err = w.Write(payload)
if err != nil {
log.Println(err)
}
}
// authMiddleware check the authentication token before accessing to the resource
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)
})
}
// 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)
return
}
if !user.IsAdmin {
forbidden(w, r)
return
}
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 {
internalServerError(w, r)
}
}()
next.ServeHTTP(w, r)
})
}

View File

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