package main import ( "bufio" "fmt" "io" "io/ioutil" "net/http" "os" "os/exec" "strconv" "strings" "time" "github.com/unixvoid/glogger" "golang.org/x/crypto/sha3" "gopkg.in/gcfg.v1" "gopkg.in/redis.v5" ) type Config struct { NethackLauncher struct { Loglevel string ServerDisplay string NethackVersion string InProgressDir string // TODO: depricate this UserDir string // TODO: depricate this HackDir string NhdatLocation string ReclistLocation string BootstrapDelay time.Duration } Redis struct { Host string Password string } } var ( config = Config{} ) func main() { // read conf file readConf() // init config file and logger initLogger() // init redis connection redisClient, redisErr := initRedisConnection() if redisErr != nil { glogger.Debug.Printf("redis connection cannot be made, trying again in %s second(s)\n", config.NethackLauncher.BootstrapDelay*time.Second) time.Sleep(config.NethackLauncher.BootstrapDelay * time.Second) redisClient, redisErr = initRedisConnection() if redisErr != nil { glogger.Error.Println("redis connection cannot be made, exiting.") panic(redisErr) } } else { glogger.Debug.Println("connection to redis succeeded.") } // create initial files needed by nethack createInitialFiles() // start homescreen screenFunction := printWelcomeScreen(redisClient) fmt.Printf("screen %s recieved\n", screenFunction) } func readConf() { // init config file err := gcfg.ReadFileInto(&config, "config.gcfg") if err != nil { panic(fmt.Sprintf("Could not load config.gcfg, error: %s\n", err)) } } func initLogger() { // init logger if config.NethackLauncher.Loglevel == "debug" { glogger.LogInit(os.Stdout, os.Stdout, os.Stdout, os.Stderr) } else if config.NethackLauncher.Loglevel == "cluster" { glogger.LogInit(os.Stdout, os.Stdout, ioutil.Discard, os.Stderr) } else if config.NethackLauncher.Loglevel == "info" { glogger.LogInit(os.Stdout, ioutil.Discard, ioutil.Discard, os.Stderr) } else { glogger.LogInit(ioutil.Discard, ioutil.Discard, ioutil.Discard, os.Stderr) } } func initRedisConnection() (*redis.Client, error) { // init redis connection redisClient := redis.NewClient(&redis.Options{ Addr: config.Redis.Host, Password: config.Redis.Password, DB: 0, }) _, redisErr := redisClient.Ping().Result() return redisClient, redisErr } func checkFiles() { // make sure record file exists if _, err := os.Stat(fmt.Sprintf("%s/record", config.NethackLauncher.HackDir)); os.IsNotExist(err) { glogger.Info.Printf("record file not found in %s/record\n", config.NethackLauncher.HackDir) fmt.Printf("%s\n", err) os.Exit(1) } // make sure reclist bin exists if _, err := os.Stat(config.NethackLauncher.ReclistLocation); os.IsNotExist(err) { glogger.Info.Printf("reclist binary not found in %s\n", config.NethackLauncher.ReclistLocation) fmt.Printf("%s\n", err) os.Exit(1) } } func clearScreen() { cmd := exec.Command("clear") cmd.Stdout = os.Stdout cmd.Run() } func printWelcomeScreen(redisClient *redis.Client) string { clearScreen() fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) println("") println(" Not logged in.") println("") println(" l) Login") println(" r) Register new user") println(" w) Watch games in progress") println(" q) Quit") println("") fmt.Printf(">> ") // disable input buffering exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run() // do not display entered characters on the screen exec.Command("stty", "-F", "/dev/tty", "-echo").Run() var b []byte = make([]byte, 1) for { os.Stdin.Read(b) switch string(b) { case "l": // restart display exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() printLoginScreen(redisClient) case "r": // restart display exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() printRegisterScreen(redisClient) case "w": clearScreen() printProgressScreen(redisClient) case "q": exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() os.Exit(0) default: } } } func printUserScreen(redisClient *redis.Client, username string) string { clearScreen() fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) println("") fmt.Printf(" Logged in as: %s\n", username) println("") println(" l) Logout") println(" w) Watch games in progress") println(" e) Edit config") fmt.Printf(" p) Play NetHack %s\n", config.NethackLauncher.NethackVersion) println(" q) Quit") println("") fmt.Printf(">> ") // disable input buffering exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run() // do not display entered characters on the screen exec.Command("stty", "-F", "/dev/tty", "-echo").Run() var b []byte = make([]byte, 1) for { os.Stdin.Read(b) switch string(b) { case "l": // restart display exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() printWelcomeScreen(redisClient) case "w": clearScreen() printProgressScreen(redisClient) case "p": // TODO save to inprogress // SADD user to an inprogress redis set // SET inprogress:user and expire it to 10 seconds // start goroutine that keeps upping expire time every 8 or so seconds // when goroutine exists, the key will be allowed to expire // have a janitor that watches inprogress set // janitor looks at every user in 'inprogress' and checks if the key remains // if the key is gone, janitor kills the user from redis set // restart display nhCommand := fmt.Sprintf("nethack -d %s -u %s\n", config.NethackLauncher.HackDir, username) ttyrecPath := fmt.Sprintf("%s/user/%s/ttyrec/yeet.ttyrec", config.NethackLauncher.HackDir, username) exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() nh := exec.Command("ttyrec", ttyrecPath, "-e", nhCommand) nh.Stdout = os.Stdout nh.Stdin = os.Stdin nh.Stderr = os.Stderr nh.Run() exec.Command("exit").Run() printUserScreen(redisClient, username) case "q": clearScreen() os.Exit(0) default: } } } func printLoginScreen(redisClient *redis.Client) { fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) println("") println(" Please enter your username. (blank entry aborts)") println("") fmt.Printf(">> ") scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { if scanner.Text() == "" { printWelcomeScreen(redisClient) } // check redis for user username := scanner.Text() storedHash, err := redisClient.Get(fmt.Sprintf("user:%s", username)).Result() if err != nil { // user does not exist fmt.Printf(" There was a problem with your last entry.\n>> ") } else { // get password from user and compare // turn off echo display exec.Command("stty", "-F", "/dev/tty", "-echo").Run() noPass := true for noPass { fmt.Printf("\n Please enter your password (blank entry aborts).\n>> ") reader := bufio.NewReader(os.Stdin) typedAuth, _ := reader.ReadString('\n') typedAuth = strings.Replace(typedAuth, "\n", "", -1) if typedAuth == "" { // exit to main menu printWelcomeScreen(redisClient) } // get hash of typedAuth typedHash := sha3.Sum512([]byte(typedAuth)) if fmt.Sprintf("%x", typedHash) == storedHash { // user authed printUserScreen(redisClient, username) noPass = false } else { fmt.Printf("\n There was a problem with your last entry.") } } } } if err := scanner.Err(); err != nil { fmt.Printf("%s\n", err) } } func printRegisterScreen(redisClient *redis.Client) { // TODO : configure password restriction checking fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) println("") println(" Welcome new user. Please enter a username") println(" Only characters and numbers are allowed, with no spaces.") println(" 20 characters max. (blank entry aborts)") println("") fmt.Printf(">> ") scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { if scanner.Text() == "" { printWelcomeScreen(redisClient) } // check redis for user username := scanner.Text() _, err := redisClient.Get(fmt.Sprintf("user:%s", username)).Result() if err != redis.Nil { // user already exists fmt.Printf("There was a problem with your last entry.\n>> ") } else { // set up user // turn off echo display exec.Command("stty", "-F", "/dev/tty", "-echo").Run() reader := bufio.NewReader(os.Stdin) noPass := true sec := "" for noPass { // pull pass the first time fmt.Printf(" Please enter your password (blank entry aborts).\n>> ") sec0, _ := reader.ReadString('\n') sec0 = strings.Replace(sec0, "\n", "", -1) if sec0 == "" { printWelcomeScreen(redisClient) } // pull pass the second time fmt.Printf("\n Please enter your password again.\n>> ") sec1, _ := reader.ReadString('\n') sec1 = strings.Replace(sec1, "\n", "", -1) // make sure passwords match if sec0 == sec1 { sec = sec0 noPass = false } else { fmt.Println("Lets try that again.") } } // reset display exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() // set user in redis secHash := sha3.Sum512([]byte(sec)) redisClient.Set(fmt.Sprintf("user:%s", username), fmt.Sprintf("%x", secHash), 0).Err() // TODO create config file // HackDir/user/username/ // HackDir/user/username/ttyrec/ // create user directories userPath := fmt.Sprintf("%s/user/%s/ttyrec/", config.NethackLauncher.HackDir, username) exec.Command("mkdir", "-p", userPath).Run() // back to main screen printUserScreen(redisClient, username) } } if err := scanner.Err(); err != nil { fmt.Printf("%s\n", err) } } func printProgressScreen(redisClient *redis.Client) { // TODO check if user is logged in and take them back to login screen after blank entry // print header fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) println("") // check directory for live players isEmpty, err := checkDir(config.NethackLauncher.InProgressDir) if err != nil { panic(err) } if isEmpty { println(" No live players currently (blank entry returns)") } else { println(" Choose a player to spectate ('enter' without selection returns)") } println("") // populate inProg map // create map for storing in progress data inProg := make(map[int]string) inProgTimer := 1 progFiles, _ := ioutil.ReadDir(config.NethackLauncher.InProgressDir) for _, f := range progFiles { // grab username and path into user[] user := strings.Split(f.Name(), ":") // get resolution lines from file res0 := "" res1 := "" filePath := fmt.Sprintf("%s/%s", config.NethackLauncher.InProgressDir, f.Name()) fileIO, err := os.OpenFile(filePath, os.O_RDWR, 0600) if err != nil { panic(err) } defer fileIO.Close() rawBytes, err := ioutil.ReadAll(fileIO) if err != nil { panic(err) } lines := strings.Split(string(rawBytes), "\n") for i, line := range lines { if i == 1 { res1 = line } if i == 2 { res0 = line } } // print user line fmt.Printf(" %d) %s\t%sx%s\t%s\n", inProgTimer, user[0], res0, res1, user[1]) // add user line to map inProg[inProgTimer] = f.Name() inProgTimer++ } println("") fmt.Printf(">> ") // start user input // disable input buffering exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run() // do not display entered characters on the screen exec.Command("stty", "-F", "/dev/tty", "-echo").Run() var b []byte = make([]byte, 1) for { os.Stdin.Read(b) s, _ := strconv.Atoi(string(b)) // check if user is trying to navigate if string(b) == "\n" { exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() printWelcomeScreen(redisClient) } // check if selection is in out map if inProg[s] != "" { user := strings.Split(inProg[s], ":") fmt.Printf("going to spectate '%s'\n", user[0]) // set ttyrec path ttyrecPath := fmt.Sprintf("%s/%s/%s/ttyrec/%s:%s:%s.ttyrec", config.NethackLauncher.UserDir, user[0][0:1], user[0], user[1], user[2], user[3]) // restart display exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() nh := exec.Command("ttyplay", "-p", ttyrecPath) nh.Stdout = os.Stdout nh.Stdin = os.Stdin nh.Stderr = os.Stderr nh.Run() // TODO add username to this //printUserScreen(redisClient, username) } } } func createInitialFiles() { // create necessary directories if they dont exist if _, err := os.Stat(config.NethackLauncher.HackDir); os.IsNotExist(err) { os.Mkdir(config.NethackLauncher.HackDir, os.ModeDir) } if _, err := os.Stat(fmt.Sprintf("%s/dumps/", config.NethackLauncher.HackDir)); os.IsNotExist(err) { os.Mkdir(fmt.Sprintf("%s/dumps/", config.NethackLauncher.HackDir), os.ModeDir) } if _, err := os.Stat(fmt.Sprintf("%s/save/", config.NethackLauncher.HackDir)); os.IsNotExist(err) { os.Mkdir(fmt.Sprintf("%s/save/", config.NethackLauncher.HackDir), os.ModeDir) } // create necessary files if they dont exist os.OpenFile(fmt.Sprintf("%s/logfile", config.NethackLauncher.HackDir), os.O_RDONLY|os.O_CREATE, 0666) os.OpenFile(fmt.Sprintf("%s/perm", config.NethackLauncher.HackDir), os.O_RDONLY|os.O_CREATE, 0666) os.OpenFile(fmt.Sprintf("%s/record", config.NethackLauncher.HackDir), os.O_RDONLY|os.O_CREATE, 0666) os.OpenFile(fmt.Sprintf("%s/xlogfile", config.NethackLauncher.HackDir), os.O_RDONLY|os.O_CREATE, 0666) // move in nhdat file if it does not exist if _, err := os.Stat(fmt.Sprintf("%s/nhdat", config.NethackLauncher.HackDir)); os.IsNotExist(err) { exec.Command("cp", config.NethackLauncher.NhdatLocation, config.NethackLauncher.HackDir).Run() } } func gethighscore(w http.ResponseWriter, r *http.Request) { // run script output, err := exec.Command(config.NethackLauncher.ReclistLocation, "-f", fmt.Sprintf("%s/record", config.NethackLauncher.HackDir)).CombinedOutput() if err != nil { fmt.Printf("%s\n", err) } fmt.Fprintf(w, "%s\n", output) } func checkDir(dirName string) (bool, error) { f, err := os.Open(dirName) if err != nil { return false, err } defer f.Close() _, err = f.Readdirnames(1) if err == io.EOF { return true, nil } return false, err }