mirror of
https://github.com/Mezeporta/Erupe.git
synced 2025-12-14 16:04:38 +01:00
implement new sign server
This commit is contained in:
@@ -89,6 +89,10 @@
|
|||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"Port": 53312
|
"Port": 53312
|
||||||
},
|
},
|
||||||
|
"NewSign": {
|
||||||
|
"Enabled": true,
|
||||||
|
"Port": 8080
|
||||||
|
},
|
||||||
"Channel": {
|
"Channel": {
|
||||||
"Enabled": true
|
"Enabled": true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type Config struct {
|
|||||||
Database Database
|
Database Database
|
||||||
Launcher Launcher
|
Launcher Launcher
|
||||||
Sign Sign
|
Sign Sign
|
||||||
|
NewSign NewSign
|
||||||
Channel Channel
|
Channel Channel
|
||||||
Entrance Entrance
|
Entrance Entrance
|
||||||
}
|
}
|
||||||
@@ -99,6 +100,12 @@ type Sign struct {
|
|||||||
Port int
|
Port int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewSign holds the new sign server config
|
||||||
|
type NewSign struct {
|
||||||
|
Enabled bool
|
||||||
|
Port int
|
||||||
|
}
|
||||||
|
|
||||||
type Channel struct {
|
type Channel struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
}
|
}
|
||||||
|
|||||||
21
main.go
21
main.go
@@ -13,6 +13,7 @@ import (
|
|||||||
"erupe-ce/server/discordbot"
|
"erupe-ce/server/discordbot"
|
||||||
"erupe-ce/server/entranceserver"
|
"erupe-ce/server/entranceserver"
|
||||||
"erupe-ce/server/launcherserver"
|
"erupe-ce/server/launcherserver"
|
||||||
|
"erupe-ce/server/newsignserver"
|
||||||
"erupe-ce/server/signserver"
|
"erupe-ce/server/signserver"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
@@ -166,6 +167,22 @@ func main() {
|
|||||||
logger.Info("Started sign server")
|
logger.Info("Started sign server")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New Sign server
|
||||||
|
var newSignServer *newsignserver.Server
|
||||||
|
if config.ErupeConfig.NewSign.Enabled {
|
||||||
|
newSignServer = newsignserver.NewServer(
|
||||||
|
&newsignserver.Config{
|
||||||
|
Logger: logger.Named("sign"),
|
||||||
|
ErupeConfig: config.ErupeConfig,
|
||||||
|
DB: db,
|
||||||
|
})
|
||||||
|
err = newSignServer.Start()
|
||||||
|
if err != nil {
|
||||||
|
preventClose(fmt.Sprintf("Failed to start new sign server: %s", err.Error()))
|
||||||
|
}
|
||||||
|
logger.Info("Started new sign server")
|
||||||
|
}
|
||||||
|
|
||||||
var channels []*channelserver.Server
|
var channels []*channelserver.Server
|
||||||
|
|
||||||
if config.ErupeConfig.Channel.Enabled {
|
if config.ErupeConfig.Channel.Enabled {
|
||||||
@@ -229,6 +246,10 @@ func main() {
|
|||||||
signServer.Shutdown()
|
signServer.Shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.ErupeConfig.NewSign.Enabled {
|
||||||
|
newSignServer.Shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
if config.ErupeConfig.Entrance.Enabled {
|
if config.ErupeConfig.Entrance.Enabled {
|
||||||
entranceServer.Shutdown()
|
entranceServer.Shutdown()
|
||||||
}
|
}
|
||||||
|
|||||||
121
server/newsignserver/dbutils.go
Normal file
121
server/newsignserver/dbutils.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package newsignserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"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) {
|
||||||
|
token := randSeq(16)
|
||||||
|
_, err := s.db.ExecContext(ctx, "INSERT INTO sign_sessions (user_id, token) VALUES ($1, $2)", uid, token)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return token, 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
|
||||||
|
}
|
||||||
218
server/newsignserver/endpoints.go
Normal file
218
server/newsignserver/endpoints.go
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
package newsignserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lib/pq"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func randSeq(n int) string {
|
||||||
|
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||||
|
b := make([]rune, n)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letters[rand.Intn(len(letters))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
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{}{})
|
||||||
|
}
|
||||||
89
server/newsignserver/newsign_server.go
Normal file
89
server/newsignserver/newsign_server.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package newsignserver
|
||||||
|
|
||||||
|
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.NewSign.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))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user