Merge pull request #117 from ZeruLight/feature/screenshot-api

Feature/screenshot api
This commit is contained in:
stratic-dev
2024-03-20 19:49:10 +00:00
committed by GitHub
11 changed files with 277 additions and 78 deletions

3
.gitignore vendored
View File

@@ -7,4 +7,5 @@ savedata/*/
*.exe *.exe
*.lnk *.lnk
*.bat *.bat
/docker/db-data /docker/db-data
screenshots/*

View File

@@ -9,7 +9,13 @@
], ],
"PatchServerManifest": "", "PatchServerManifest": "",
"PatchServerFile": "", "PatchServerFile": "",
"ScreenshotAPIURL": "", "Screenshots":{
"Enabled":true,
"Host":"127.0.0.1",
"Port":8080,
"OutputDir":"screenshots",
"UploadQuality":100
},
"DeleteOnSaveCorruption": false, "DeleteOnSaveCorruption": false,
"ClientMode": "ZZ", "ClientMode": "ZZ",
"QuestCacheExpiry": 300, "QuestCacheExpiry": 300,
@@ -189,8 +195,8 @@
"Enabled": true, "Enabled": true,
"Port": 53312 "Port": 53312
}, },
"SignV2": { "API": {
"Enabled": false, "Enabled": true,
"Port": 8080, "Port": 8080,
"PatchServer": "", "PatchServer": "",
"Banners": [], "Banners": [],

View File

@@ -75,7 +75,6 @@ type Config struct {
LoginNotices []string // MHFML string of the login notices displayed LoginNotices []string // MHFML string of the login notices displayed
PatchServerManifest string // Manifest patch server override PatchServerManifest string // Manifest patch server override
PatchServerFile string // File patch server override PatchServerFile string // File patch server override
ScreenshotAPIURL string // Destination for screenshots uploaded to BBS
DeleteOnSaveCorruption bool // Attempts to save corrupted data will flag the save for deletion DeleteOnSaveCorruption bool // Attempts to save corrupted data will flag the save for deletion
ClientMode string ClientMode string
RealClientMode Mode RealClientMode Mode
@@ -87,16 +86,18 @@ type Config struct {
EarthID int32 EarthID int32
EarthMonsters []int32 EarthMonsters []int32
SaveDumps SaveDumpOptions SaveDumps SaveDumpOptions
DebugOptions DebugOptions Screenshots ScreenshotsOptions
GameplayOptions GameplayOptions
Discord Discord DebugOptions DebugOptions
Commands []Command GameplayOptions GameplayOptions
Courses []Course Discord Discord
Database Database Commands []Command
Sign Sign Courses []Course
SignV2 SignV2 Database Database
Channel Channel Sign Sign
Entrance Entrance API API
Channel Channel
Entrance Entrance
} }
type SaveDumpOptions struct { type SaveDumpOptions struct {
@@ -105,6 +106,14 @@ type SaveDumpOptions struct {
OutputDir string OutputDir string
} }
type ScreenshotsOptions struct {
Enabled bool
Host string // Destination for screenshots uploaded to BBS
Port uint32 // Port for screenshots API
OutputDir string
UploadQuality int //Determines the upload quality to the server
}
// DebugOptions holds various debug/temporary options for use while developing Erupe. // DebugOptions holds various debug/temporary options for use while developing Erupe.
type DebugOptions struct { type DebugOptions struct {
CleanDB bool // Automatically wipes the DB on server reset. CleanDB bool // Automatically wipes the DB on server reset.
@@ -228,29 +237,29 @@ type Sign struct {
Port int Port int
} }
// SignV2 holds the new sign server config // API holds server config
type SignV2 struct { type API struct {
Enabled bool Enabled bool
Port int Port int
PatchServer string PatchServer string
Banners []SignV2Banner Banners []APISignBanner
Messages []SignV2Message Messages []APISignMessage
Links []SignV2Link Links []APISignLink
} }
type SignV2Banner struct { type APISignBanner struct {
Src string `json:"src"` // Displayed image URL Src string `json:"src"` // Displayed image URL
Link string `json:"link"` // Link accessed on click Link string `json:"link"` // Link accessed on click
} }
type SignV2Message struct { type APISignMessage struct {
Message string `json:"message"` // Displayed message Message string `json:"message"` // Displayed message
Date int64 `json:"date"` // Displayed date Date int64 `json:"date"` // Displayed date
Kind int `json:"kind"` // 0 for 'Default', 1 for 'New' Kind int `json:"kind"` // 0 for 'Default', 1 for 'New'
Link string `json:"link"` // Link accessed on click Link string `json:"link"` // Link accessed on click
} }
type SignV2Link struct { type APISignLink struct {
Name string `json:"name"` // Displayed name Name string `json:"name"` // Displayed name
Icon string `json:"icon"` // Displayed icon. It will be cast as a monochrome color as long as it is transparent. Icon string `json:"icon"` // Displayed icon. It will be cast as a monochrome color as long as it is transparent.
Link string `json:"link"` // Link accessed on click Link string `json:"link"` // Link accessed on click

22
main.go
View File

@@ -10,11 +10,11 @@ import (
"syscall" "syscall"
"time" "time"
"erupe-ce/server/api"
"erupe-ce/server/channelserver" "erupe-ce/server/channelserver"
"erupe-ce/server/discordbot" "erupe-ce/server/discordbot"
"erupe-ce/server/entranceserver" "erupe-ce/server/entranceserver"
"erupe-ce/server/signserver" "erupe-ce/server/signserver"
"erupe-ce/server/signv2server"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
_ "github.com/lib/pq" _ "github.com/lib/pq"
@@ -181,21 +181,21 @@ func main() {
} }
// New Sign server // New Sign server
var newSignServer *signv2server.Server var ApiServer *api.APIServer
if config.SignV2.Enabled { if config.API.Enabled {
newSignServer = signv2server.NewServer( ApiServer = api.NewAPIServer(
&signv2server.Config{ &api.Config{
Logger: logger.Named("sign"), Logger: logger.Named("sign"),
ErupeConfig: _config.ErupeConfig, ErupeConfig: _config.ErupeConfig,
DB: db, DB: db,
}) })
err = newSignServer.Start() err = ApiServer.Start()
if err != nil { if err != nil {
preventClose(fmt.Sprintf("SignV2: Failed to start, %s", err.Error())) preventClose(fmt.Sprintf("API: Failed to start, %s", err.Error()))
} }
logger.Info("SignV2: Started successfully") logger.Info("API: Started successfully")
} else { } else {
logger.Info("SignV2: Disabled") logger.Info("API: Disabled")
} }
var channels []*channelserver.Server var channels []*channelserver.Server
@@ -273,8 +273,8 @@ func main() {
signServer.Shutdown() signServer.Shutdown()
} }
if config.SignV2.Enabled { if config.API.Enabled {
newSignServer.Shutdown() ApiServer.Shutdown()
} }
if config.Entrance.Enabled { if config.Entrance.Enabled {

View File

@@ -1,8 +1,8 @@
package signv2server package api
import ( import (
"context" "context"
"erupe-ce/config" _config "erupe-ce/config"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
@@ -21,8 +21,8 @@ type Config struct {
ErupeConfig *_config.Config ErupeConfig *_config.Config
} }
// Server is the MHF custom launcher sign server. // APIServer is Erupes Standard API interface
type Server struct { type APIServer struct {
sync.Mutex sync.Mutex
logger *zap.Logger logger *zap.Logger
erupeConfig *_config.Config erupeConfig *_config.Config
@@ -31,9 +31,9 @@ type Server struct {
isShuttingDown bool isShuttingDown bool
} }
// NewServer creates a new Server type. // NewAPIServer creates a new Server type.
func NewServer(config *Config) *Server { func NewAPIServer(config *Config) *APIServer {
s := &Server{ s := &APIServer{
logger: config.Logger, logger: config.Logger,
erupeConfig: config.ErupeConfig, erupeConfig: config.ErupeConfig,
db: config.DB, db: config.DB,
@@ -43,7 +43,7 @@ func NewServer(config *Config) *Server {
} }
// Start starts the server in a new goroutine. // Start starts the server in a new goroutine.
func (s *Server) Start() error { func (s *APIServer) Start() error {
// Set up the routes responsible for serving the launcher HTML, serverlist, unique name check, and JP auth. // Set up the routes responsible for serving the launcher HTML, serverlist, unique name check, and JP auth.
r := mux.NewRouter() r := mux.NewRouter()
r.HandleFunc("/launcher", s.Launcher) r.HandleFunc("/launcher", s.Launcher)
@@ -52,9 +52,11 @@ func (s *Server) Start() error {
r.HandleFunc("/character/create", s.CreateCharacter) r.HandleFunc("/character/create", s.CreateCharacter)
r.HandleFunc("/character/delete", s.DeleteCharacter) r.HandleFunc("/character/delete", s.DeleteCharacter)
r.HandleFunc("/character/export", s.ExportSave) r.HandleFunc("/character/export", s.ExportSave)
r.HandleFunc("/api/ss/bbs/upload.php", s.ScreenShot)
r.HandleFunc("/api/ss/bbs/{id}", s.ScreenShotGet)
handler := handlers.CORS(handlers.AllowedHeaders([]string{"Content-Type"}))(r) handler := handlers.CORS(handlers.AllowedHeaders([]string{"Content-Type"}))(r)
s.httpServer.Handler = handlers.LoggingHandler(os.Stdout, handler) s.httpServer.Handler = handlers.LoggingHandler(os.Stdout, handler)
s.httpServer.Addr = fmt.Sprintf(":%d", s.erupeConfig.SignV2.Port) s.httpServer.Addr = fmt.Sprintf(":%d", s.erupeConfig.API.Port)
serveError := make(chan error, 1) serveError := make(chan error, 1)
go func() { go func() {
@@ -74,7 +76,7 @@ func (s *Server) Start() error {
} }
// Shutdown exits the server gracefully. // Shutdown exits the server gracefully.
func (s *Server) Shutdown() { func (s *APIServer) Shutdown() {
s.logger.Debug("Shutting down") s.logger.Debug("Shutting down")
s.Lock() s.Lock()

View File

@@ -1,4 +1,4 @@
package signv2server package api
import ( import (
"context" "context"
@@ -10,7 +10,7 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
func (s *Server) createNewUser(ctx context.Context, username string, password string) (uint32, uint32, error) { func (s *APIServer) createNewUser(ctx context.Context, username string, password string) (uint32, uint32, error) {
// Create salted hash of user password // Create salted hash of user password
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil { if err != nil {
@@ -32,7 +32,7 @@ func (s *Server) createNewUser(ctx context.Context, username string, password st
return id, rights, err return id, rights, err
} }
func (s *Server) createLoginToken(ctx context.Context, uid uint32) (uint32, string, error) { func (s *APIServer) createLoginToken(ctx context.Context, uid uint32) (uint32, string, error) {
loginToken := token.Generate(16) loginToken := token.Generate(16)
var tid uint32 var tid uint32
err := s.db.QueryRowContext(ctx, "INSERT INTO sign_sessions (user_id, token) VALUES ($1, $2) RETURNING id", uid, loginToken).Scan(&tid) err := s.db.QueryRowContext(ctx, "INSERT INTO sign_sessions (user_id, token) VALUES ($1, $2) RETURNING id", uid, loginToken).Scan(&tid)
@@ -42,7 +42,7 @@ func (s *Server) createLoginToken(ctx context.Context, uid uint32) (uint32, stri
return tid, loginToken, nil return tid, loginToken, nil
} }
func (s *Server) userIDFromToken(ctx context.Context, token string) (uint32, error) { func (s *APIServer) userIDFromToken(ctx context.Context, token string) (uint32, error) {
var userID uint32 var userID uint32
err := s.db.QueryRowContext(ctx, "SELECT user_id FROM sign_sessions WHERE token = $1", token).Scan(&userID) err := s.db.QueryRowContext(ctx, "SELECT user_id FROM sign_sessions WHERE token = $1", token).Scan(&userID)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@@ -53,7 +53,7 @@ func (s *Server) userIDFromToken(ctx context.Context, token string) (uint32, err
return userID, nil return userID, nil
} }
func (s *Server) createCharacter(ctx context.Context, userID uint32) (Character, error) { func (s *APIServer) createCharacter(ctx context.Context, userID uint32) (Character, error) {
var character Character var character Character
err := s.db.GetContext(ctx, &character, err := s.db.GetContext(ctx, &character,
"SELECT id, name, is_female, weapon_type, hr, gr, last_login FROM characters WHERE is_new_character = true AND user_id = $1 LIMIT 1", "SELECT id, name, is_female, weapon_type, hr, gr, last_login FROM characters WHERE is_new_character = true AND user_id = $1 LIMIT 1",
@@ -78,7 +78,7 @@ func (s *Server) createCharacter(ctx context.Context, userID uint32) (Character,
return character, err return character, err
} }
func (s *Server) deleteCharacter(ctx context.Context, userID uint32, charID uint32) error { func (s *APIServer) deleteCharacter(ctx context.Context, userID uint32, charID uint32) error {
var isNew bool var isNew bool
err := s.db.QueryRow("SELECT is_new_character FROM characters WHERE id = $1", charID).Scan(&isNew) err := s.db.QueryRow("SELECT is_new_character FROM characters WHERE id = $1", charID).Scan(&isNew)
if err != nil { if err != nil {
@@ -92,7 +92,7 @@ func (s *Server) deleteCharacter(ctx context.Context, userID uint32, charID uint
return err return err
} }
func (s *Server) getCharactersForUser(ctx context.Context, uid uint32) ([]Character, error) { func (s *APIServer) getCharactersForUser(ctx context.Context, uid uint32) ([]Character, error) {
var characters []Character var characters []Character
err := s.db.SelectContext( err := s.db.SelectContext(
ctx, &characters, ` ctx, &characters, `
@@ -107,7 +107,7 @@ func (s *Server) getCharactersForUser(ctx context.Context, uid uint32) ([]Charac
return characters, nil return characters, nil
} }
func (s *Server) getReturnExpiry(uid uint32) time.Time { func (s *APIServer) getReturnExpiry(uid uint32) time.Time {
var returnExpiry, lastLogin time.Time var returnExpiry, lastLogin time.Time
s.db.Get(&lastLogin, "SELECT COALESCE(last_login, now()) FROM users WHERE id=$1", uid) s.db.Get(&lastLogin, "SELECT COALESCE(last_login, now()) FROM users WHERE id=$1", uid)
if time.Now().Add((time.Hour * 24) * -90).After(lastLogin) { if time.Now().Add((time.Hour * 24) * -90).After(lastLogin) {
@@ -124,7 +124,7 @@ func (s *Server) getReturnExpiry(uid uint32) time.Time {
return returnExpiry return returnExpiry
} }
func (s *Server) exportSave(ctx context.Context, uid uint32, cid uint32) (map[string]interface{}, error) { func (s *APIServer) exportSave(ctx context.Context, uid uint32, cid uint32) (map[string]interface{}, error) {
row := s.db.QueryRowxContext(ctx, "SELECT * FROM characters WHERE id=$1 AND user_id=$2", cid, uid) row := s.db.QueryRowxContext(ctx, "SELECT * FROM characters WHERE id=$1 AND user_id=$2", cid, uid)
result := make(map[string]interface{}) result := make(map[string]interface{})
err := row.MapScan(result) err := row.MapScan(result)

View File

@@ -1,15 +1,24 @@
package signv2server package api
import ( import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"encoding/xml"
"errors" "errors"
_config "erupe-ce/config" _config "erupe-ce/config"
"erupe-ce/server/channelserver" "erupe-ce/server/channelserver"
"fmt"
"image"
"image/jpeg"
"io"
"net/http" "net/http"
"os"
"path/filepath"
"regexp"
"strings" "strings"
"time" "time"
"github.com/gorilla/mux"
"github.com/lib/pq" "github.com/lib/pq"
"go.uber.org/zap" "go.uber.org/zap"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@@ -21,9 +30,9 @@ const (
) )
type LauncherResponse struct { type LauncherResponse struct {
Banners []_config.SignV2Banner `json:"banners"` Banners []_config.APISignBanner `json:"banners"`
Messages []_config.SignV2Message `json:"messages"` Messages []_config.APISignMessage `json:"messages"`
Links []_config.SignV2Link `json:"links"` Links []_config.APISignLink `json:"links"`
} }
type User struct { type User struct {
@@ -66,7 +75,7 @@ type ExportData struct {
Character map[string]interface{} `json:"character"` Character map[string]interface{} `json:"character"`
} }
func (s *Server) newAuthData(userID uint32, userRights uint32, userTokenID uint32, userToken string, characters []Character) AuthData { func (s *APIServer) newAuthData(userID uint32, userRights uint32, userTokenID uint32, userToken string, characters []Character) AuthData {
resp := AuthData{ resp := AuthData{
CurrentTS: uint32(channelserver.TimeAdjusted().Unix()), CurrentTS: uint32(channelserver.TimeAdjusted().Unix()),
ExpiryTS: uint32(s.getReturnExpiry(userID).Unix()), ExpiryTS: uint32(s.getReturnExpiry(userID).Unix()),
@@ -77,7 +86,7 @@ func (s *Server) newAuthData(userID uint32, userRights uint32, userTokenID uint3
Token: userToken, Token: userToken,
}, },
Characters: characters, Characters: characters,
PatchServer: s.erupeConfig.SignV2.PatchServer, PatchServer: s.erupeConfig.API.PatchServer,
Notices: []string{}, Notices: []string{},
} }
if s.erupeConfig.DebugOptions.MaxLauncherHR { if s.erupeConfig.DebugOptions.MaxLauncherHR {
@@ -103,16 +112,16 @@ func (s *Server) newAuthData(userID uint32, userRights uint32, userTokenID uint3
return resp return resp
} }
func (s *Server) Launcher(w http.ResponseWriter, r *http.Request) { func (s *APIServer) Launcher(w http.ResponseWriter, r *http.Request) {
var respData LauncherResponse var respData LauncherResponse
respData.Banners = s.erupeConfig.SignV2.Banners respData.Banners = s.erupeConfig.API.Banners
respData.Messages = s.erupeConfig.SignV2.Messages respData.Messages = s.erupeConfig.API.Messages
respData.Links = s.erupeConfig.SignV2.Links respData.Links = s.erupeConfig.API.Links
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(respData) json.NewEncoder(w).Encode(respData)
} }
func (s *Server) Login(w http.ResponseWriter, r *http.Request) { func (s *APIServer) Login(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
var reqData struct { var reqData struct {
Username string `json:"username"` Username string `json:"username"`
@@ -164,7 +173,7 @@ func (s *Server) Login(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(respData) json.NewEncoder(w).Encode(respData)
} }
func (s *Server) Register(w http.ResponseWriter, r *http.Request) { func (s *APIServer) Register(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
var reqData struct { var reqData struct {
Username string `json:"username"` Username string `json:"username"`
@@ -204,7 +213,7 @@ func (s *Server) Register(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(respData) json.NewEncoder(w).Encode(respData)
} }
func (s *Server) CreateCharacter(w http.ResponseWriter, r *http.Request) { func (s *APIServer) CreateCharacter(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
var reqData struct { var reqData struct {
Token string `json:"token"` Token string `json:"token"`
@@ -233,7 +242,7 @@ func (s *Server) CreateCharacter(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(character) json.NewEncoder(w).Encode(character)
} }
func (s *Server) DeleteCharacter(w http.ResponseWriter, r *http.Request) { func (s *APIServer) DeleteCharacter(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
var reqData struct { var reqData struct {
Token string `json:"token"` Token string `json:"token"`
@@ -258,7 +267,7 @@ func (s *Server) DeleteCharacter(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(struct{}{}) json.NewEncoder(w).Encode(struct{}{})
} }
func (s *Server) ExportSave(w http.ResponseWriter, r *http.Request) { func (s *APIServer) ExportSave(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
var reqData struct { var reqData struct {
Token string `json:"token"` Token string `json:"token"`
@@ -286,3 +295,118 @@ func (s *Server) ExportSave(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(save) json.NewEncoder(w).Encode(save)
} }
func (s *APIServer) ScreenShotGet(w http.ResponseWriter, r *http.Request) {
// Get the 'id' parameter from the URL
token := mux.Vars(r)["id"]
var tokenPattern = regexp.MustCompile(`[A-Za-z0-9]+`)
if !tokenPattern.MatchString(token) || token == "" {
http.Error(w, "Not Valid Token", http.StatusBadRequest)
}
// Open the image file
safePath := s.erupeConfig.Screenshots.OutputDir
path := filepath.Join(safePath, fmt.Sprintf("%s.jpg", token))
result, err := verifyPath(path, safePath)
if err != nil {
fmt.Println("Error " + err.Error())
} else {
fmt.Println("Canonical: " + result)
file, err := os.Open(result)
if err != nil {
http.Error(w, "Image not found", http.StatusNotFound)
return
}
defer file.Close()
// Set content type header to image/jpeg
w.Header().Set("Content-Type", "image/jpeg")
// Copy the image content to the response writer
if _, err := io.Copy(w, file); err != nil {
http.Error(w, "Unable to send image", http.StatusInternalServerError)
return
}
}
}
func (s *APIServer) ScreenShot(w http.ResponseWriter, r *http.Request) {
// Create a struct representing the XML result
type Result struct {
XMLName xml.Name `xml:"result"`
Code string `xml:"code"`
}
// Set the Content-Type header to specify that the response is in XML format
w.Header().Set("Content-Type", "text/xml")
result := Result{Code: "200"}
if !s.erupeConfig.Screenshots.Enabled {
result = Result{Code: "400"}
} else {
if r.Method != http.MethodPost {
result = Result{Code: "405"}
}
// Get File from Request
file, _, err := r.FormFile("img")
if err != nil {
result = Result{Code: "400"}
}
var tokenPattern = regexp.MustCompile(`[A-Za-z0-9]+`)
token := r.FormValue("token")
if !tokenPattern.MatchString(token) || token == "" {
result = Result{Code: "401"}
}
// Validate file
img, _, err := image.Decode(file)
if err != nil {
result = Result{Code: "400"}
}
safePath := s.erupeConfig.Screenshots.OutputDir
path := filepath.Join(safePath, fmt.Sprintf("%s.jpg", token))
verified, err := verifyPath(path, safePath)
if err != nil {
result = Result{Code: "500"}
} else {
_, err = os.Stat(safePath)
if err != nil {
if os.IsNotExist(err) {
err = os.MkdirAll(safePath, os.ModePerm)
if err != nil {
s.logger.Error("Error writing screenshot, could not create folder")
result = Result{Code: "500"}
}
} else {
s.logger.Error("Error writing screenshot")
result = Result{Code: "500"}
}
}
// Create or open the output file
outputFile, err := os.Create(verified)
if err != nil {
result = Result{Code: "500"}
}
defer outputFile.Close()
// Encode the image and write it to the file
err = jpeg.Encode(outputFile, img, &jpeg.Options{Quality: s.erupeConfig.Screenshots.UploadQuality})
if err != nil {
s.logger.Error("Error writing screenshot, could not write file", zap.Error(err))
result = Result{Code: "500"}
}
}
}
// Marshal the struct into XML
xmlData, err := xml.Marshal(result)
if err != nil {
http.Error(w, "Unable to marshal XML", http.StatusInternalServerError)
return
}
// Write the XML response with a 200 status code
w.WriteHeader(http.StatusOK)
w.Write(xmlData)
}

37
server/api/utils.go Normal file
View File

@@ -0,0 +1,37 @@
package api
import (
"errors"
"fmt"
"path/filepath"
)
func inTrustedRoot(path string, trustedRoot string) error {
for path != "/" {
path = filepath.Dir(path)
if path == trustedRoot {
return nil
}
}
return errors.New("path is outside of trusted root")
}
func verifyPath(path string, trustedRoot string) (string, error) {
c := filepath.Clean(path)
fmt.Println("Cleaned path: " + c)
r, err := filepath.EvalSymlinks(c)
if err != nil {
fmt.Println("Error " + err.Error())
return c, errors.New("Unsafe or invalid path specified")
}
err = inTrustedRoot(r, trustedRoot)
if err != nil {
fmt.Println("Error " + err.Error())
return r, errors.New("Unsafe or invalid path specified")
} else {
return r, nil
}
}

View File

@@ -7,35 +7,47 @@ import (
"erupe-ce/network/mhfpacket" "erupe-ce/network/mhfpacket"
) )
// Handler BBS handles all the interactions with the for the screenshot sending to bulitin board functionality. For it to work it requires the API to be hosted somehwere. This implementation supports discord.
// Checks the status of the user to see if they can use Bulitin Board yet
func handleMsgMhfGetBbsUserStatus(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfGetBbsUserStatus(s *Session, p mhfpacket.MHFPacket) {
//Post Screenshot pauses till this succeedes
pkt := p.(*mhfpacket.MsgMhfGetBbsUserStatus) pkt := p.(*mhfpacket.MsgMhfGetBbsUserStatus)
bf := byteframe.NewByteFrame() bf := byteframe.NewByteFrame()
bf.WriteUint32(200) bf.WriteUint32(200) //HTTP Status Codes //200 Success //404 You wont be able to post for a certain amount of time after creating your character //401/500 A error occured server side
bf.WriteUint32(0) bf.WriteUint32(0)
bf.WriteUint32(0) bf.WriteUint32(0)
bf.WriteUint32(0) bf.WriteUint32(0)
doAckBufSucceed(s, pkt.AckHandle, bf.Data()) doAckBufSucceed(s, pkt.AckHandle, bf.Data())
} }
// Checks the status of Bultin Board Server to see if authenticated
func handleMsgMhfGetBbsSnsStatus(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfGetBbsSnsStatus(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfGetBbsSnsStatus) pkt := p.(*mhfpacket.MsgMhfGetBbsSnsStatus)
bf := byteframe.NewByteFrame() bf := byteframe.NewByteFrame()
bf.WriteUint32(200) bf.WriteUint32(200) //200 Success //4XX Authentication has expired Please re-authenticate //5XX
bf.WriteUint32(401) bf.WriteUint32(401) //unk http status?
bf.WriteUint32(401) bf.WriteUint32(401) //unk http status?
bf.WriteUint32(0) bf.WriteUint32(0)
doAckBufSucceed(s, pkt.AckHandle, bf.Data()) doAckBufSucceed(s, pkt.AckHandle, bf.Data())
} }
// Tells the game client what host port and gives the bultin board article a token
func handleMsgMhfApplyBbsArticle(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfApplyBbsArticle(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfApplyBbsArticle) pkt := p.(*mhfpacket.MsgMhfApplyBbsArticle)
bf := byteframe.NewByteFrame() bf := byteframe.NewByteFrame()
articleToken := token.Generate(40) articleToken := token.Generate(40)
bf.WriteUint32(200)
bf.WriteUint32(80) bf.WriteUint32(200) //http status //200 success //4XX An error occured server side
bf.WriteUint32(s.server.erupeConfig.Screenshots.Port)
bf.WriteUint32(0) bf.WriteUint32(0)
bf.WriteUint32(0) bf.WriteUint32(0)
bf.WriteBytes(stringsupport.PaddedString(articleToken, 64, false)) bf.WriteBytes(stringsupport.PaddedString(articleToken, 64, false))
bf.WriteBytes(stringsupport.PaddedString(s.server.erupeConfig.ScreenshotAPIURL, 64, false)) bf.WriteBytes(stringsupport.PaddedString(s.server.erupeConfig.Screenshots.Host, 64, false))
//pkt.unk1[3] == Changes sometimes?
if s.server.erupeConfig.Screenshots.Enabled && s.server.erupeConfig.Discord.Enabled {
s.server.DiscordScreenShotSend(pkt.Name, pkt.Title, pkt.Description, articleToken)
}
doAckBufSucceed(s, pkt.AckHandle, bf.Data()) doAckBufSucceed(s, pkt.AckHandle, bf.Data())
} }

View File

@@ -367,6 +367,14 @@ func (s *Server) DiscordChannelSend(charName string, content string) {
} }
} }
func (s *Server) DiscordScreenShotSend(charName string, title string, description string, articleToken string) {
if s.erupeConfig.Discord.Enabled && s.discordBot != nil {
imageUrl := fmt.Sprintf("%s:%d/api/ss/bbs/%s", s.erupeConfig.Screenshots.Host, s.erupeConfig.Screenshots.Port, articleToken)
message := fmt.Sprintf("**%s**: %s - %s %s", charName, title, description, imageUrl)
s.discordBot.RealtimeChannelSend(message)
}
}
func (s *Server) FindSessionByCharID(charID uint32) *Session { func (s *Server) FindSessionByCharID(charID uint32) *Session {
for _, c := range s.Channels { for _, c := range s.Channels {
for _, session := range c.sessions { for _, session := range c.sessions {

View File

@@ -1,10 +1,11 @@
package discordbot package discordbot
import ( import (
"erupe-ce/config" _config "erupe-ce/config"
"regexp"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"go.uber.org/zap" "go.uber.org/zap"
"regexp"
) )
var Commands = []*discordgo.ApplicationCommand{ var Commands = []*discordgo.ApplicationCommand{
@@ -113,7 +114,6 @@ func (bot *DiscordBot) RealtimeChannelSend(message string) (err error) {
return return
} }
func ReplaceTextAll(text string, regex *regexp.Regexp, handler func(input string) string) string { func ReplaceTextAll(text string, regex *regexp.Regexp, handler func(input string) string) string {
result := regex.ReplaceAllFunc([]byte(text), func(s []byte) []byte { result := regex.ReplaceAllFunc([]byte(text), func(s []byte) []byte {
input := regex.ReplaceAllString(string(s), `$1`) input := regex.ReplaceAllString(string(s), `$1`)