/* Package door: a go implementation of a BBS door for linux and Windows that uses door32.sys, understand CP437 and unicode (if available), detects screen size, and supports TheDraw Fonts. import ( "door" ) int main() { d = door.Door{} d.Init() // Process commandline switches, initialize door, detect screen size. d.Write("Welcome to my awesome door, written in "+door.ColorText("BLINK BOLD WHITE")+"go"+door.Reset+"."+door.CRNL) d.Write("Press a key...") d.Key() d.Write(door.CRNL) } */ package door import ( "bufio" "flag" "fmt" "log" "os" "path/filepath" "strconv" "strings" "sync" "time" "runtime/debug" "sync/atomic" ) const SavePos = "\x1b[s" // Save Cursor Position const RestorePos = "\x1b[u" // Restore Cursor Position const CRNL = "\r\n" // BBS Line Ending const Clrscr = "\x1b[0m\x1b[2J\x1b[H" // Clear screen, home cursor const HideCursor = "\x1b[?25l" // Hide Cursor const ShowCursor = "\x1b[?25h" // Show Cursor var Reset string = Color(0) // ANSI Color Reset var Unicode bool // Unicode support detected var CP437 bool // CP437 support detected var Full_CP437 bool // Full CP437 support detected (handles control codes properly) var Height int // Screen height detected var Width int // Screen width detected var Inactivity int64 = 120 // Inactivity timeout /* door32.sys: 0 Line 1 : Comm type (0=local, 1=serial, 2=telnet) 0 Line 2 : Comm or socket handle 38400 Line 3 : Baud rate Mystic 1.07 Line 4 : BBSID (software name and version) 1 Line 5 : User record position (1-based) James Coyle Line 6 : User's real name g00r00 Line 7 : User's handle/alias 255 Line 8 : User's security level 58 Line 9 : User's time left (in minutes) 1 Line 10: Emulation *See Below 1 Line 11: Current node number */ // Door32 information type DropfileConfig struct { Comm_type int // (not used) Comm_handle int // Handle to use to talk to the user Baudrate int // (not used) BBSID string // BBS Software name User_number int // User number Real_name string // User's Real Name Handle string // User's Handle/Nick Security_level int // Security Level (if given) Time_left int // Time Left (minutes) Emulation int // (not used) Node int // BBS Node number } type Door struct { Config DropfileConfig READFD int WRITEFD int Disconnected int32 // atomic bool // Has User disconnected/Hung up? TimeOut time.Time // Fixed point in time, when time expires StartTime time.Time // Time when User started door Pushback FIFOBuffer LastColor []int readerChannel chan byte // Reading from the User writerChannel chan string // Writing to the User closeChannel chan bool // Closing wg sync.WaitGroup writerMutex sync.Mutex } // Return the amount of time left as time.Duration func (d *Door) TimeLeft() time.Duration { return time.Until(d.TimeOut) } func (d *Door) TimeUsed() time.Duration { return time.Since(d.StartTime) } func (d *Door) Disconnect() bool { return atomic.LoadInt32(&d.Disconnected) != 0 } // Read the BBS door file. We only support door32.sys. func (d *Door) ReadDropfile(filename string) { file, err := os.Open(filename) if err != nil { log.Panicf("Open(%s): %s\n", filename, err) } defer file.Close() var lines []string // read line by line // The scanner handles DOS and linux file endings. scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() lines = append(lines, line) } d.Config.Comm_type, err = strconv.Atoi(lines[0]) if err != nil { log.Panicf("Door32 Comm Type (expected integer): %s\n", err) } d.Config.Comm_handle, err = strconv.Atoi(lines[1]) if err != nil { log.Panicf("Door32 Comm Handle (expected integer): %s\n", err) } d.Config.Baudrate, err = strconv.Atoi(lines[2]) if err != nil { log.Panicf("Door32 Baudrate (expected integer): %s\n", err) } d.Config.BBSID = lines[3] d.Config.User_number, err = strconv.Atoi(lines[4]) if err != nil { log.Panicf("Door32 User Number (expected integer): %s\n", err) } d.Config.Real_name = lines[5] d.Config.Handle = lines[6] d.Config.Security_level, err = strconv.Atoi(lines[7]) if err != nil { log.Panicf("Door32 Security Level (expected integer): %s\n", err) } d.Config.Time_left, err = strconv.Atoi(lines[8]) if err != nil { log.Panicf("Door32 Time Left (expected integer): %s\n", err) } d.Config.Emulation, err = strconv.Atoi(lines[9]) if err != nil { log.Panicf("Door32 Emulation (expected integer): %s\n", err) } d.Config.Node, err = strconv.Atoi(lines[10]) if err != nil { log.Panicf("Door32 Node Number (expected integer): %s\n", err) } d.READFD = d.Config.Comm_handle d.WRITEFD = d.Config.Comm_handle d.StartTime = time.Now() // Calculate when time expires. d.TimeOut = time.Now().Add(time.Duration(d.Config.Time_left) * time.Minute) } // Detect client terminal capabilities, Unicode, CP437, Full_CP437, // screen Height and Width. // Full_CP437 means the terminal understands control codes as CP437. func (d *Door) detect() { d.Write("\x1b[0;30;40m\x1b[2J\x1b[H") // black on black, clrscr, go home d.Write("\x03\x04\x1b[6n") // hearts and diamonds does CP437 work? d.Write(CRNL + "\u2615\x1b[6n") // hot beverage d.Write("\x1b[999C\x1b[999B\x1b[6n" + Reset + "\x1b[2J\x1b[H") // goto end of screen + cursor pos time.Sleep(250 * time.Millisecond) // read everything var results string for { r := d.getch() if r < 0 { break } results += string(byte(r)) } if len(results) > 0 { output := strings.Replace(results, "\x1b", "^[", -1) log.Println("DETECT:", output) } else { log.Println("DETECT: Nothing received.") return } if (strings.Contains(results, "1;1R") || strings.Contains(results, "1;3R")) && (strings.Contains(results, "2:2R") || strings.Contains(results, "2;3R")) { Unicode = true } else { Unicode = false CP437 = true } if strings.Contains(results, "1;3R") { Full_CP437 = true } // get screen size pos := strings.LastIndex(results, "\x1b") if pos != -1 { pos++ if results[pos] == '[' { pos++ results = results[pos:] pos = strings.Index(results, ";") if pos != -1 { height := results[:pos] Height, _ = strconv.Atoi(height) pos++ results = results[pos:] pos = strings.Index(results, "R") if pos != -1 { width := results[:pos] Width, _ = strconv.Atoi(width) } } else { Height = 0 Width = 0 } } } log.Printf("Unicode %v Screen: %d X %d\n", Unicode, Width, Height) } // Initialize door framework. Parse commandline, read dropfile, // detect terminal capabilities. func (d *Door) Init(doorname string) { var dropfile string d.Pushback = NewFIFOBuffer(5) // Get path to binary, and chdir to it. binaryPath, _ := os.Executable() binaryPath = filepath.Dir(binaryPath) _ = os.Chdir(binaryPath) flag.StringVar(&dropfile, "d", "", "Path to dropfile") flag.Parse() if len(dropfile) == 0 { flag.PrintDefaults() os.Exit(2) } d.ReadDropfile(dropfile) // Logfile will be doorname - node # logfilename := fmt.Sprintf("%s-%d.log", doorname, d.Config.Node) logf, err := os.OpenFile(logfilename, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666) if err != nil { log.Panicf("Error creating log file %s: %v", logfilename, err) } log.SetOutput(logf) log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) log.Printf("Loading dropfile %s\n", dropfile) log.Printf("BBS %s, User %s / Handle %s / File %d\n", d.Config.BBSID, d.Config.Real_name, d.Config.Handle, d.Config.Comm_handle) d.readerChannel = make(chan byte, 8) d.writerChannel = make(chan string) d.closeChannel = make(chan bool, 2) // reader & door.Close d.setupChannels() d.detect() if Unicode { BOXES = BOXES_UNICODE BARS = BARS_UNICODE } else { BOXES = BOXES_CP437 BARS = BARS_CP437 } } func (d *Door) Close() { defer func() { if err := recover(); err != nil { log.Println("door.Close FAILURE:", err) // This displays stack trace stderr debug.PrintStack() } }() log.Println("Closing...") d.closeChannel <- true if ! d.Disconnect() { // log.Println("close write channel") // close(d.writerChannel) log.Println("close read handle") CloseReader(d.Config.Comm_handle) log.Println("closed read handle") } // d.closeChannel <- true // close(d.closeChannel) log.Println("wg.Wait()") d.wg.Wait() log.Println("Closed.") } // Goto X, Y - Position the cursor using ANSI Escape Codes // // Example: // d.Write(door.Goto(1, 5) + "Now at X=1, Y=5.") func Goto(x int, y int) string { return fmt.Sprintf("\x1b[%d;%dH", y, x) }