Merge pull request #52 from rockisch/newsign

implement new sign server
This commit is contained in:
wish
2022-11-07 08:38:06 +11:00
committed by GitHub
6 changed files with 451 additions and 0 deletions

View File

@@ -90,6 +90,10 @@
"Enabled": true,
"Port": 53312
},
"SignV2": {
"Enabled": false,
"Port": 8080
},
"Channel": {
"Enabled": true
},

View File

@@ -30,6 +30,7 @@ type Config struct {
Database Database
Launcher Launcher
Sign Sign
SignV2 SignV2
Channel Channel
Entrance Entrance
}
@@ -100,6 +101,12 @@ type Sign struct {
Port int
}
// SignV2 holds the new sign server config
type SignV2 struct {
Enabled bool
Port int
}
type Channel struct {
Enabled bool
}

21
main.go
View File

@@ -14,6 +14,7 @@ import (
"erupe-ce/server/entranceserver"
"erupe-ce/server/launcherserver"
"erupe-ce/server/signserver"
"erupe-ce/server/signv2server"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
@@ -166,6 +167,22 @@ func main() {
logger.Info("Started sign server")
}
// New Sign server
var newSignServer *signv2server.Server
if config.ErupeConfig.SignV2.Enabled {
newSignServer = signv2server.NewServer(
&signv2server.Config{
Logger: logger.Named("sign"),
ErupeConfig: config.ErupeConfig,
DB: db,
})
err = newSignServer.Start()
if err != nil {
preventClose(fmt.Sprintf("Failed to start sign-v2 server: %s", err.Error()))
}
logger.Info("Started new sign server")
}
var channels []*channelserver.Server
if config.ErupeConfig.Channel.Enabled {
@@ -229,6 +246,10 @@ func main() {
signServer.Shutdown()
}
if config.ErupeConfig.SignV2.Enabled {
newSignServer.Shutdown()
}
if config.ErupeConfig.Entrance.Enabled {
entranceServer.Shutdown()
}

View File

@@ -0,0 +1,122 @@
package signv2server
import (
"context"
"database/sql"
"erupe-ce/common/token"
"fmt"
"time"
"golang.org/x/crypto/bcrypt"
)
func (s *Server) createNewUser(ctx context.Context, username string, password string) (int, error) {
// Create salted hash of user password
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return 0, err
}
var id int
err = s.db.QueryRowContext(
ctx, `
INSERT INTO users (username, password, return_expires)
VALUES ($1, $2, $3)
RETURNING id
`,
username, string(passwordHash), time.Now().Add(time.Hour*24*30),
).Scan(&id)
return id, err
}
func (s *Server) createLoginToken(ctx context.Context, uid int) (string, error) {
loginToken := token.Generate(16)
_, err := s.db.ExecContext(ctx, "INSERT INTO sign_sessions (user_id, token) VALUES ($1, $2)", uid, loginToken)
if err != nil {
return "", err
}
return loginToken, nil
}
func (s *Server) userIDFromToken(ctx context.Context, token string) (int, error) {
var userID int
err := s.db.QueryRowContext(ctx, "SELECT user_id FROM sign_sessions WHERE token = $1", token).Scan(&userID)
if err == sql.ErrNoRows {
return 0, fmt.Errorf("invalid login token")
} else if err != nil {
return 0, err
}
return userID, nil
}
func (s *Server) createCharacter(ctx context.Context, userID int) (int, error) {
var charID int
err := s.db.QueryRowContext(ctx,
"SELECT id FROM characters WHERE is_new_character = true AND user_id = $1",
userID,
).Scan(&charID)
if err == sql.ErrNoRows {
err = s.db.QueryRowContext(ctx, `
INSERT INTO characters (
user_id, is_female, is_new_character, name, unk_desc_string,
hrp, gr, weapon_type, last_login
)
VALUES ($1, false, true, '', '', 0, 0, 0, $2)
RETURNING id`,
userID, uint32(time.Now().Unix()),
).Scan(&charID)
}
return charID, err
}
func (s *Server) deleteCharacter(ctx context.Context, userID int, charID int) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.ExecContext(
ctx, `
DELETE FROM login_boost_state
WHERE char_id = $1`,
charID,
)
if err != nil {
return err
}
_, err = tx.ExecContext(
ctx, `
DELETE FROM guild_characters
WHERE character_id = $1`,
charID,
)
if err != nil {
return err
}
_, err = tx.ExecContext(
ctx, `
DELETE FROM characters
WHERE user_id = $1 AND id = $2`,
userID, charID,
)
if err != nil {
return err
}
return tx.Commit()
}
func (s *Server) getCharactersForUser(ctx context.Context, uid int) ([]Character, error) {
characters := make([]Character, 0)
err := s.db.SelectContext(
ctx, &characters, `
SELECT id, name, is_female, weapon_type, hrp, gr, last_login
FROM characters
WHERE user_id = $1 AND deleted = false AND is_new_character = false ORDER BY id ASC`,
uid,
)
if err != nil {
return nil, err
}
return characters, nil
}

View File

@@ -0,0 +1,208 @@
package signv2server
import (
"database/sql"
"encoding/json"
"errors"
"net/http"
"time"
"github.com/lib/pq"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
)
type LauncherMessage struct {
Message string `json:"message"`
Date int64 `json:"date"`
Link string `json:"link"`
}
type Character struct {
ID int `json:"id"`
Name string `json:"name"`
IsFemale bool `json:"isFemale" db:"is_female"`
Weapon int `json:"weapon" db:"weapon_type"`
HR int `json:"hr" db:"hrp"`
GR int `json:"gr"`
LastLogin int64 `json:"lastLogin" db:"last_login"`
}
func (s *Server) Launcher(w http.ResponseWriter, r *http.Request) {
var respData struct {
Important []LauncherMessage `json:"important"`
Normal []LauncherMessage `json:"normal"`
}
respData.Important = []LauncherMessage{
{
Message: "Server Update 9 Released!",
Date: time.Date(2022, 8, 2, 0, 0, 0, 0, time.UTC).Unix(),
Link: "https://discord.com/channels/368424389416583169/929509970624532511/1003985850255818762",
},
{
Message: "Eng 2.0 & Ravi Patch Released!",
Date: time.Date(2022, 5, 3, 0, 0, 0, 0, time.UTC).Unix(),
Link: "https://discord.com/channels/368424389416583169/929509970624532511/969305400795078656",
},
{
Message: "Launcher Patch V1.0 Released!",
Date: time.Date(2022, 4, 24, 0, 0, 0, 0, time.UTC).Unix(),
Link: "https://discord.com/channels/368424389416583169/929509970624532511/969286397301248050",
},
}
respData.Normal = []LauncherMessage{
{
Message: "Join the community Discord for updates!",
Date: time.Date(2022, 4, 24, 0, 0, 0, 0, time.UTC).Unix(),
Link: "https://discord.gg/CFnzbhQ",
},
}
w.WriteHeader(200)
w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(respData)
}
func (s *Server) Login(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var reqData struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&reqData); err != nil {
s.logger.Error("JSON decode error", zap.Error(err))
w.WriteHeader(400)
w.Write([]byte("Invalid data received"))
return
}
var (
userID int
password string
)
err := s.db.QueryRow("SELECT id, password FROM users WHERE username = $1", reqData.Username).Scan(&userID, &password)
if err == sql.ErrNoRows {
w.WriteHeader(400)
w.Write([]byte("Username does not exist"))
return
} else if err != nil {
s.logger.Warn("SQL query error", zap.Error(err))
w.WriteHeader(500)
return
}
if bcrypt.CompareHashAndPassword([]byte(password), []byte(reqData.Password)) != nil {
w.WriteHeader(400)
w.Write([]byte("Your password is incorrect"))
return
}
var respData struct {
Token string `json:"token"`
Characters []Character `json:"characters"`
}
respData.Token, err = s.createLoginToken(ctx, userID)
if err != nil {
s.logger.Warn("Error registering login token", zap.Error(err))
w.WriteHeader(500)
return
}
respData.Characters, err = s.getCharactersForUser(ctx, userID)
if err != nil {
s.logger.Warn("Error getting characters from DB", zap.Error(err))
w.WriteHeader(500)
return
}
w.WriteHeader(200)
w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(respData)
}
func (s *Server) Register(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var reqData struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&reqData); err != nil {
s.logger.Error("JSON decode error", zap.Error(err))
w.WriteHeader(400)
w.Write([]byte("Invalid data received"))
return
}
s.logger.Info("Creating account", zap.String("username", reqData.Username))
userID, err := s.createNewUser(ctx, reqData.Username, reqData.Password)
if err != nil {
var pqErr *pq.Error
if errors.As(err, &pqErr) && pqErr.Constraint == "users_username_key" {
w.WriteHeader(400)
w.Write([]byte("User already exists"))
return
}
s.logger.Error("Error checking user", zap.Error(err), zap.String("username", reqData.Username))
w.WriteHeader(500)
return
}
var respData struct {
Token string `json:"token"`
}
respData.Token, err = s.createLoginToken(ctx, userID)
if err != nil {
s.logger.Error("Error registering login token", zap.Error(err))
w.WriteHeader(500)
return
}
json.NewEncoder(w).Encode(respData)
}
func (s *Server) CreateCharacter(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var reqData struct {
Token string `json:"token"`
}
if err := json.NewDecoder(r.Body).Decode(&reqData); err != nil {
s.logger.Error("JSON decode error", zap.Error(err))
w.WriteHeader(400)
w.Write([]byte("Invalid data received"))
return
}
var respData struct {
CharID int `json:"id"`
}
userID, err := s.userIDFromToken(ctx, reqData.Token)
if err != nil {
w.WriteHeader(401)
return
}
respData.CharID, err = s.createCharacter(ctx, userID)
if err != nil {
s.logger.Error("Failed to create character", zap.Error(err), zap.String("token", reqData.Token))
w.WriteHeader(500)
return
}
json.NewEncoder(w).Encode(respData)
}
func (s *Server) DeleteCharacter(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var reqData struct {
Token string `json:"token"`
CharID int `json:"id"`
}
if err := json.NewDecoder(r.Body).Decode(&reqData); err != nil {
s.logger.Error("JSON decode error", zap.Error(err))
w.WriteHeader(400)
w.Write([]byte("Invalid data received"))
return
}
userID, err := s.userIDFromToken(ctx, reqData.Token)
if err != nil {
w.WriteHeader(401)
return
}
if err := s.deleteCharacter(ctx, userID, reqData.CharID); err != nil {
s.logger.Error("Failed to delete character", zap.Error(err), zap.String("token", reqData.Token), zap.Int("charID", reqData.CharID))
w.WriteHeader(500)
return
}
json.NewEncoder(w).Encode(struct{}{})
}

View File

@@ -0,0 +1,89 @@
package signv2server
import (
"context"
"erupe-ce/config"
"fmt"
"net/http"
"os"
"sync"
"time"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
)
type Config struct {
Logger *zap.Logger
DB *sqlx.DB
ErupeConfig *config.Config
}
// Server is the MHF custom launcher sign server.
type Server struct {
sync.Mutex
logger *zap.Logger
erupeConfig *config.Config
db *sqlx.DB
httpServer *http.Server
isShuttingDown bool
}
// NewServer creates a new Server type.
func NewServer(config *Config) *Server {
s := &Server{
logger: config.Logger,
erupeConfig: config.ErupeConfig,
db: config.DB,
httpServer: &http.Server{},
}
return s
}
// Start starts the server in a new goroutine.
func (s *Server) Start() error {
// Set up the routes responsible for serving the launcher HTML, serverlist, unique name check, and JP auth.
r := mux.NewRouter()
r.HandleFunc("/launcher", s.Launcher)
r.HandleFunc("/login", s.Login)
r.HandleFunc("/register", s.Register)
r.HandleFunc("/character/create", s.CreateCharacter)
r.HandleFunc("/character/delete", s.DeleteCharacter)
handler := handlers.CORS(handlers.AllowedHeaders([]string{"Content-Type"}))(r)
s.httpServer.Handler = handlers.LoggingHandler(os.Stdout, handler)
s.httpServer.Addr = fmt.Sprintf(":%d", s.erupeConfig.SignV2.Port)
serveError := make(chan error, 1)
go func() {
if err := s.httpServer.ListenAndServe(); err != nil {
// Send error if any.
serveError <- err
}
}()
// Get the error from calling ListenAndServe, otherwise assume it's good after 250 milliseconds.
select {
case err := <-serveError:
return err
case <-time.After(250 * time.Millisecond):
return nil
}
}
// Shutdown exits the server gracefully.
func (s *Server) Shutdown() {
s.logger.Debug("Shutting down")
s.Lock()
s.isShuttingDown = true
s.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := s.httpServer.Shutdown(ctx); err != nil {
// Just warn because we are shutting down the server anyway.
s.logger.Warn("Got error on httpServer shutdown", zap.Error(err))
}
}