// NGnius 2020-02-27 package main import ( "bytes" "encoding/json" "flag" "fmt" "io/ioutil" "net/http" "os" "os/signal" "path/filepath" ) const ( defaultPassword = "" defaultEntryURL = "http://localhost:1337/record" defaultPort = "9000" defaultDir = "criterias" ) var ( // cli params password string entryURL string port string dir string // internal server *http.Server client *http.Client isClosing bool referenceCriteria map[string]Criteria ) // json structs // NewEntryJSON a new entry to be saved to a leaderboard type NewEntryJSON struct { // from leadercraft-s Score int64 PlayerID int64 BoardID int64 Password string } // Criteria a set of values that must be met type Criteria struct { Location [2][3]float64 // (min coords (x,y,z), max coords (x,y,z)) Time int64 // time since start of game (seconds) GameID int64 // game/board id in leadercraft-s and in Gamecraft (workshop ID?) PlayerID int64 // player id in leadercraft-s and in Gamecraft (steam ID) Coefficient float64 // coefficient for calculating the score Complete bool // game has been completed Points int64 // points scored in game ScoreMode string // score calculation mode } func (c *Criteria) Meets(c2 *Criteria) bool { meets := false if c2.Location[0][0] != 0.0 && c2.Location[0][1] != 0.0 && c2.Location[0][2] != 0.0 && c2.Location[1][0] != 0.0 && c2.Location[1][1] != 0.0 && c2.Location[1][2] != 0.0 { // criteria is location-based meets = c.Location[0][0] >= c2.Location[0][0] && c.Location[0][1] >= c2.Location[0][1] && c.Location[0][2] >= c2.Location[0][2] meets = meets && c.Location[1][0] <= c2.Location[1][0] && c.Location[1][1] <= c2.Location[1][1] && c.Location[1][2] <= c2.Location[1][2] return meets } if c2.Complete { return c.Complete && (c.Points >= c2.Points) } return meets } func (c *Criteria) Score(c2 *Criteria) int64 { if c2.ScoreMode == "time" { time := float64(c.Time) if time < 1 { time = 1 } return int64(c2.Coefficient / float64(c.Time)) } if c2.ScoreMode == "points" { return int64(c2.Coefficient * float64(c.Points)) } return 0 } func init() { flag.StringVar(&password, "entry-pwd", defaultPassword, "Password for new entry POST requests") flag.StringVar(&entryURL, "url", defaultEntryURL, "URL for new entry POST requests") flag.StringVar(&port, "port", defaultPort, "Port to listen on") flag.StringVar(&dir, "dir", defaultDir, "Working directory") } func main() { flag.Parse() serverMux := http.NewServeMux() serverMux.HandleFunc("/criteria", criteriaHandler) signalChan := make(chan os.Signal) signal.Notify(signalChan, os.Interrupt) go func() { s := <-signalChan fmt.Println("Received terminate signal " + s.String()) isClosing = true server.Close() }() server = &http.Server{ Addr: ":" + port, Handler: serverMux, } client = &http.Client{} fmt.Println("Starting on " + server.Addr) err := server.ListenAndServe() if err != nil && !isClosing { fmt.Println(err) } } // criteria POST request handler // this also sends a new entry to leadercraft-s when the criteria is met func criteriaHandler(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { w.WriteHeader(405) return } authHeader := r.Header.Get("Authorization") w.Header().Add("Content-Type", "application/json") //w.Header().Add("Access-Control-Allow-Origin", "*") data, readErr := ioutil.ReadAll(r.Body) if readErr != nil { // body could not be read properly w.WriteHeader(500) return } reqCriteria := &Criteria{} unmarshErr := json.Unmarshal(data, reqCriteria) if unmarshErr != nil { // body could not be interpreted as json w.WriteHeader(400) return } if reqCriteria.GameID < 2 { //fmt.Println("404 -- GameID too low") w.WriteHeader(404) return } criteriaFilename := filepath.Join(dir, fmt.Sprintf("criteria-%d.json", reqCriteria.GameID)) //fmt.Println(criteriaFilename) realCriteria := &Criteria{} realCriteriaF, openErr := os.Open(criteriaFilename) if openErr != nil { // file not found (or not accessible) //fmt.Println("404 -- criteria file not accessible: %s", openErr.Error()) w.WriteHeader(404) return } realCritData, realCritReadErr := ioutil.ReadAll(realCriteriaF) if realCritReadErr != nil { // internal read error w.WriteHeader(500) return } unmarshCritErr := json.Unmarshal(realCritData, realCriteria) if unmarshCritErr != nil { // criteria file is invalid json //fmt.Printf("404 -- Invalid criteria file json: %s", unmarshCritErr.Error()) w.WriteHeader(404) return } // TODO check if criteria matches if !reqCriteria.Meets(realCriteria) { // if criteria does not match, stop //fmt.Println("400 -- Criteria does not meet required criteria") w.WriteHeader(400) return } // if criteria matches, send new entry to leadercraft-s entry := NewEntryJSON{ Score: reqCriteria.Score(realCriteria), PlayerID: reqCriteria.PlayerID, BoardID: realCriteria.GameID, Password: password, } echoData, marshErr := json.Marshal(entry) if marshErr != nil { //fmt.Println("500 -- Unable to marshal entry into JSON for leaderboard-s endpoint") w.WriteHeader(500) return } echoBody := bytes.NewReader(echoData) entryReq, reqErr := http.NewRequest("POST", entryURL, echoBody) if reqErr != nil { // malformed request parameters //fmt.Println("500 -- Malformed request detected during initialization") w.WriteHeader(500) return } entryReq.Header.Add("Authorization", authHeader) entryReq.Header.Add("Content-Type", "application/json") echoResp, postErr := client.Do(entryReq) if postErr != nil { // bad communication or malformed request //fmt.Println("500 -- Bad communication for leadercraft-s") w.WriteHeader(500) return } // echo new entry request response to original sender w.WriteHeader(echoResp.StatusCode) echoRespData, echoReadErr := ioutil.ReadAll(echoResp.Body) if echoReadErr != nil { // body read error (should never occur) //fmt.Println("!!! Error reading response body from leadercraft-s") return } w.Write(echoRespData) }