package main // TODO split up into multiple files import ( "bufio" "fmt" "io" "io/ioutil" "os" "os/exec" "path/filepath" "strconv" "strings" "sync" "time" "golang.org/x/crypto/sha3" "gopkg.in/gcfg.v1" "gopkg.in/redis.v5" ) type Config struct { NethackLauncher struct { Loglevel string ServerDisplay string NethackVersion string HackDir string NhdatLocation string RecoverBinary string BootstrapDelay time.Duration } Redis struct { Host string Password string } } var ( config = Config{} wg sync.WaitGroup ) func main() { // read conf file readConf() // init redis connection redisClient, redisErr := initRedisConnection() if redisErr != nil { time.Sleep(config.NethackLauncher.BootstrapDelay * time.Second) redisClient, redisErr = initRedisConnection() if redisErr != nil { panic(redisErr) } } else { } // create initial files needed by nethack createInitialFiles() // start janitor go janitor(redisClient) // 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 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) { fmt.Printf("record file not found in %s/record\n", config.NethackLauncher.HackDir) fmt.Printf("%s\n", err) os.Exit(1) } // make sure initial rcfile exists hackRCLoc := fmt.Sprintf("%s/.nethackrc", config.NethackLauncher.HackDir) if _, err := os.Stat(hackRCLoc); os.IsNotExist(err) { fmt.Printf("initial config file not found at: %s\n", hackRCLoc) 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(" h) View highscores") 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 "h": clearScreen() printHighScores(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(" c) Change password") println(" w) Watch games in progress") println(" h) View highscores") println(" e) Edit config") println(" r) Recover from crash") 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 "e": hackRCLoc := fmt.Sprintf("%s/user/%s/.nethackrc", config.NethackLauncher.HackDir, username) exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() nh := exec.Command("vim", "-Z", hackRCLoc) nh.Stdout = os.Stdout nh.Stdin = os.Stdin nh.Stderr = os.Stderr nh.Run() clearScreen() printUserScreen(redisClient, username) case "c": printChangePasswordScreen(redisClient, username) clearScreen() case "w": clearScreen() printProgressScreen(redisClient, username) case "h": clearScreen() printHighScores(redisClient, username) case "p": wg.Add(1) currentTime := time.Now().UTC() fulltime := currentTime.Format("2006-01-02.03:04:05") go runGame(username, fulltime) watcher := startWatcher(username, fulltime, redisClient) wg.Wait() close(watcher) printUserScreen(redisClient, username) case "r": clearScreen() recoverSave(redisClient, username) case "q": exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() 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() // create user directories userPath := fmt.Sprintf("%s/user/%s/ttyrec/", config.NethackLauncher.HackDir, username) exec.Command("mkdir", "-p", userPath).Run() // copy in rc file hackRCLoc := fmt.Sprintf("%s/.nethackrc", config.NethackLauncher.HackDir) hackRCDest := fmt.Sprintf("%s/user/%s/.nethackrc", config.NethackLauncher.HackDir, username) exec.Command("cp", hackRCLoc, hackRCDest).Run() // TODO: move the above creation code into the createUserFiles() function createUserFiles(username) // back to main screen printUserScreen(redisClient, username) } } if err := scanner.Err(); err != nil { fmt.Printf("%s\n", err) } } func printChangePasswordScreen(redisClient *redis.Client, username string) { // TODO : configure password restriction checking clearScreen() fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) println("") fmt.Printf(" Welcome %s\n", 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() == "" { // printUserScreen(redisClient, username) // } // 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() // back to main screen printUserScreen(redisClient, username) //} if err := scanner.Err(); err != nil { fmt.Printf("%s\n", err) } } func printProgressScreen(redisClient *redis.Client, username string) { // print header fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) println("") // check directory for live players isNotEmpty, err := redisClient.Exists("inprogress").Result() if err != nil { panic(err) } if isNotEmpty { println(" Choose a player to spectate ('enter' without selection returns)") } else { println(" No live players currently (blank entry returns)") } println("") inProg := make(map[int]string) inProgTimer := 1 inprogress, err := redisClient.SMembers("inprogress").Result() if err != nil { panic(err) } for _, i := range inprogress { // loop through users fmt.Printf(" %d) %s\n", inProgTimer, i) // add user to line map inProg[inProgTimer] = i 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() if username == "" { printWelcomeScreen(redisClient) } else { printUserScreen(redisClient, username) } } // 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 ttyName, _ := redisClient.Get(fmt.Sprintf("inprogress:%s", user[0])).Result() ttyrecPath := fmt.Sprintf("%s/user/%s/ttyrec/%s.ttyrec", config.NethackLauncher.HackDir, user[0], ttyName) // restart display exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() nh := exec.Command("ttyplay", "-p", ttyrecPath) //nh := exec.Command("termplay", "-f", "live", ttyrecPath) nh.Stdout = os.Stdout nh.Stdin = os.Stdin nh.Stderr = os.Stderr nh.Run() if username == "" { printWelcomeScreen(redisClient) } else { printUserScreen(redisClient, username) } // TODO fix bug where user has to when game exists } } } func runGame(username, timestamp string) { exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() // put together users home dir homeDir := fmt.Sprintf("%s/user/%s/", config.NethackLauncher.HackDir, username) ttyrecPath := fmt.Sprintf("%s/user/%s/ttyrec/%s.ttyrec", config.NethackLauncher.HackDir, username, timestamp) nh := exec.Command("ttyrec", "-f", ttyrecPath, "--", "nethack") nh.Env = os.Environ() nh.Env = append(nh.Env, fmt.Sprintf("HOME=%s", homeDir)) nh.Env = append(nh.Env, fmt.Sprintf("USER=%s", username)) nh.Env = append(nh.Env, fmt.Sprintf("NETHACKDIR=%s/user/%s", config.NethackLauncher.HackDir, username)) nh.Stdout = os.Stdout nh.Stdin = os.Stdin nh.Stderr = os.Stderr err := nh.Run() if err != nil { fmt.Print(err) } exec.Command("exit").Run() wg.Done() } func startWatcher(username, timestamp string, redisClient *redis.Client) chan struct{} { // create initial keys redisClient.SAdd("inprogress", username) redisClient.Set(fmt.Sprintf("inprogress:%s", username), timestamp, 0) ch := make(chan struct{}) // enter inital inprogress yet go func() { for { select { case <-ch: return default: redisClient.Expire(fmt.Sprintf("inprogress:%s", username), 10*time.Second) time.Sleep(8 * time.Second) } } }() return ch } 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() } // make sure initial rcfile exists hackRCLoc := fmt.Sprintf("%s/.nethackrc", config.NethackLauncher.HackDir) if _, err := os.Stat(hackRCLoc); os.IsNotExist(err) { fmt.Printf("initial config file not found at: %s\n", hackRCLoc) fmt.Printf("%s\n", err) // check root and move in if applicable if _, err := os.Stat("/.nethackrc"); os.IsNotExist(err) { fmt.Printf("initial config file not found at root\n") fmt.Printf("%s\n", err) os.Exit(1) } else { // move nethackrc file to proper location exec.Command("cp", "/.nethackrc", config.NethackLauncher.HackDir).Run() } } } func createUserFiles(username string) { // create necessary directories if they dont exist userpath := fmt.Sprintf("%s/user/%s", config.NethackLauncher.HackDir, username) if _, err := os.Stat(fmt.Sprintf("%s/dumps/", userpath)); os.IsNotExist(err) { os.Mkdir(fmt.Sprintf("%s/dumps/", userpath), os.ModeDir) } if _, err := os.Stat(fmt.Sprintf("%s/save/", userpath)); os.IsNotExist(err) { os.Mkdir(fmt.Sprintf("%s/save/", userpath), os.ModeDir) } // create necessary files if they dont exist os.OpenFile(fmt.Sprintf("%s/logfile", userpath), os.O_RDONLY|os.O_CREATE, 0666) os.OpenFile(fmt.Sprintf("%s/perm", userpath), os.O_RDONLY|os.O_CREATE, 0666) os.OpenFile(fmt.Sprintf("%s/record", userpath), os.O_RDONLY|os.O_CREATE, 0666) os.OpenFile(fmt.Sprintf("%s/xlogfile", userpath), os.O_RDONLY|os.O_CREATE, 0666) // move in nhdat file if it does not exist exec.Command("cp", config.NethackLauncher.NhdatLocation, userpath).Run() } func recoverSave(redisClient *redis.Client, username string) { // if no statefile exist, exit early as we cannot recover matches, _ := filepath.Glob(fmt.Sprintf("%s/user/%s/*lock*", config.NethackLauncher.HackDir, username)) if matches == nil { fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) println("") println(" The dungeon crumbles around you, darkness remains...") println(" Godspeed adventurer..") println("") println(" No statefiles exist. (press enter to return)") println("") fmt.Printf(">> ") scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { if scanner.Text() == "" { printUserScreen(redisClient, username) } } } fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) println("") println(" Recover utility.") println("") // check if dir is empty isEmpty, _ := checkDir(fmt.Sprintf("%s/user/%s/save/", config.NethackLauncher.HackDir, username)) if !isEmpty { // save file exists, overwrite? println(" Save file already exists!") println(" Overwrite? (y/n)") println("") fmt.Printf(">> ") } else { println(" Attempt to recover statefile? (y/n)") 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 "n": clearScreen() printUserScreen(redisClient, username) case "y": // set user path to save file candidates userPath := fmt.Sprintf("%s/user/%s/", config.NethackLauncher.HackDir, username) // read in all files candidates, err := ioutil.ReadDir(userPath) if err != nil { fmt.Println(err) } var newestFile string var newestTime int64 = 0 for _, f := range candidates { // loop through save file candidates if strings.Contains(f.Name(), "lock") { fi, err := os.Stat(userPath + f.Name()) if err != nil { fmt.Println(err) } currTime := fi.ModTime().Unix() if currTime > newestTime { newestTime = currTime newestFile = f.Name() } } } // get prefix for latest file latestFile := strings.Split(newestFile, ".") //fmt.Printf("recover string: %s -d %s/user/%s %s", config.NethackLauncher.RecoverBinary, config.NethackLauncher.HackDir, username, latestFile[0]) // run recover on file out, err := exec.Command(config.NethackLauncher.RecoverBinary, "-d", fmt.Sprintf("%s/user/%s", config.NethackLauncher.HackDir, username), latestFile[0]).Output() if err != nil { fmt.Println(out) panic(err) } // make sure save file exists before removing the extra locks isEmpty, _ = checkDir(fmt.Sprintf("%s/user/%s/save/", config.NethackLauncher.HackDir, username)) if !isEmpty { // save file made it, clean up old cruft files, err := filepath.Glob(fmt.Sprintf("%s/*lock*", userPath)) if err != nil { panic(err) } for _, f := range files { if err := os.Remove(f); err != nil { panic(err) } } // alert user that the save file was recovered println("") println(" Statefile was recovered successfully! (press enter to return)") fmt.Printf(">> ") } case "\n": clearScreen() printUserScreen(redisClient, username) default: } } } func janitor(redisClient *redis.Client) { // loop through the set for { inprogress, err := redisClient.SMembers("inprogress").Result() if err != nil { panic(err) } for _, i := range inprogress { // for each user, make sure the expire key exists exists, err := redisClient.Exists(fmt.Sprintf("inprogress:%s", i)).Result() if err != nil { panic(err) } if !exists { redisClient.SRem("inprogress", i) } } time.Sleep(10 * time.Second) } } func printHighScores(redisClient *redis.Client, username string) { exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() nh := exec.Command("nethack", "-d", config.NethackLauncher.HackDir, "-s") nh.Stdout = os.Stdout nh.Stdin = os.Stdin nh.Stderr = os.Stderr nh.Run() println("") println(" Press enter to return to menu") println("") fmt.Printf(">> ") // allow user back to home screen // 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) // check if user is trying to navigate if string(b) == "\n" { exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() if username == "" { printWelcomeScreen(redisClient) } else { printUserScreen(redisClient, username) } } } } 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 }