diff --git a/README.md b/README.md new file mode 100644 index 0000000..21f8460 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# go-bbs +## a basic multiuser bulletin board system written in Go + +This is a simple framework for developing and deploying a console-based bulletin board system. + +It is currently under active development but not ready for use quite yet. \ No newline at end of file diff --git a/main.go b/main.go index f71fe20..5a238df 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,5 @@ +// go-bbs provides a basic framework for a multiuser bulletin board server application +// it is indended to be customized by the operator of the bulletin board package main import ( @@ -26,6 +28,11 @@ import ( type InChanKey string type OutChanKey string +// ioHandler is the gaio.Watcher's main loop +// it wait for I/O calls to the watcher's Read and Write methods +// and tracks the state of the call, dispatching the appropriate gaio.OpResult +// to the proper channel based on whether it was a read (input from the remote client) +// or a write (output to the remote client) func ioHandler(w *gaio.Watcher, WatcherControl *chan string) { for { select { @@ -71,6 +78,9 @@ func ioHandler(w *gaio.Watcher, WatcherControl *chan string) { } } +// main is the main loop of the server +// it sets up both the network listener as well as the gaio.Watcher that will handle +// I/O for each incoming network connection func main() { port := flag.Int("port", 3333, "Port to accept connections on.") host := flag.String("host", "127.0.0.1", "Host or IP to bind to") @@ -131,6 +141,9 @@ func main() { } } +// menuHandler is the main loop for each individual remote connection +// it receives input over the connection's InChan channel (part of its context) +// as a gaio.OpResult func menuHandler(ctx context.Context, conn net.Conn, w *gaio.Watcher) { var wg sync.WaitGroup @@ -319,10 +332,16 @@ func menuHandler(ctx context.Context, conn net.Conn, w *gaio.Watcher) { log.Println("terminating menucontrol for client") } +// screenHandler handles non-interactive commands +// it takes a screen name and an optional struct of data to be parsed into the screen's template +// it returns the populated screen as a byte array and an error if there's a problem loading the template +// screen templates are simple text files with optional go template directives. if the text file includes +// ANSI control sequences they will be passed on to the remote client as well. +// TODO: add caching of static screens, move ANSI handling out to optional template directives as well func screenHandler(screenName string, tplData interface{}) (screen []byte, err error) { var screenBuf *bytes.Buffer if tmpl, err := template.ParseFiles("screens/" + screenName + ".ans"); err != nil { - log.Printf("err loading welcome template: %v\n", err) + log.Printf("err loading welcome template: %v\n", err) // // TODO: better error handling } else { screenBuf = new(bytes.Buffer) tmpl.Execute(screenBuf, tplData) @@ -331,6 +350,11 @@ func screenHandler(screenName string, tplData interface{}) (screen []byte, err e return screenBuf.Bytes(), err } +// doorHandler handles interactive commands +// it halts the processing of the menuHandler and allows the doorHandler full duplex I/O +// between the remote connection and a local program's stdin/stdout +// doorHandler returns control to menuHandler once the program completes and exits +// TODO: add argument for program, program's prompt, launch text, and exit text func doorHandler(ctx context.Context, c net.Conn, w *gaio.Watcher, menuwg *sync.WaitGroup) error { defer menuwg.Done() @@ -340,21 +364,19 @@ func doorHandler(ctx context.Context, c net.Conn, w *gaio.Watcher, menuwg *sync. var ok bool if inChan, _, ok = openconn.FromContext(ctx); !ok { - log.Println("Could not get inChan/outChan from context!", ok, ctx) + log.Println("Could not get inChan/outChan from context!", ok, ctx) // TODO: better error handling } - cmd := exec.Command("/usr/games/adventure") + cmd := exec.Command("/usr/games/adventure") // TODO: move command path and args to an argument to doorHandler ptmx, err := pty.Start(cmd) if err != nil { return err } // Make sure to close the pty at the end. - defer func() { _ = ptmx.Close() }() // Best effort. + defer func() { _ = ptmx.Close() }() // Best effort. TODO: better error handling, better zombie handling - // Can I do this? if term.IsTerminal(int(ptmx.Fd())) { - log.Println("Making cmd terminal raw") oldState, _ := term.MakeRaw(int(ptmx.Fd())) defer term.Restore(int(ptmx.Fd()), oldState) } @@ -370,13 +392,12 @@ func doorHandler(ctx context.Context, c net.Conn, w *gaio.Watcher, menuwg *sync. select { case result, ok := <-inChan: var l int - log.Println("i") if ok { if result.Operation == gaio.OpRead && result.Size > 0 { l, err = ptmx.Write(result.Buffer[:result.Size]) if err != nil { if err == io.EOF { - log.Println("EOF on cmd.write!!!") + log.Println("EOF on cmd.write!!!") // TODO: better error handling break IOLoop } } @@ -387,20 +408,19 @@ func doorHandler(ctx context.Context, c net.Conn, w *gaio.Watcher, menuwg *sync. } } else { - log.Println("yikes, problem getting a result from inChan in doorHandler!") + log.Println("yikes, problem getting a result from inChan in doorHandler!") // TODO: better error handling } case <-Terminator: log.Println("Terminator'd!!!") break IOLoop default: if !waitingForInput { - log.Println("o") ptmx.SetReadDeadline(time.Now().Add(1 * time.Second)) var l int outBuf := make([]byte, 4096) if l, err = ptmx.Read(outBuf); err != nil { if err == io.EOF { - log.Println("EOF on cmd stdout - outta here") + // EOF isn't actually an error in this case, it just means we're done break IOLoop } else if errors.Is(err, os.ErrDeadlineExceeded) { // output deadline exceeded - get some input instead @@ -410,20 +430,19 @@ func doorHandler(ctx context.Context, c net.Conn, w *gaio.Watcher, menuwg *sync. // program exited - get outta here break IOLoop } else { - log.Printf("err reading from cmd term: %v\n", err) + log.Printf("err reading from cmd term: %v\n", err) // TODO: better error handling break IOLoop } } if l > 0 { w.Write(ctx, c, outBuf[:l]) log.Printf("wrote %d bytes to watcher: %v\n", l, string(outBuf[:l])) - if strings.Contains(string(outBuf[:l]), ">") { + if strings.Contains(string(outBuf[:l]), ">") { // TODO: abstract out prompt string as argument to doorHandler // a prompt appeared! waitingForInput = true } } else { - // zero output? let's get some input? - // waitingForInput = true + // zero output? let's get some input opportunistically w.Read(ctx, c, nil) } } @@ -435,7 +454,7 @@ func doorHandler(ctx context.Context, c net.Conn, w *gaio.Watcher, menuwg *sync. log.Println("waiting for cmd") exitErr := cmd.Wait() if exitErr != nil { - log.Printf("cmd exited with err: %v\n", exitErr) + log.Printf("cmd exited with err: %v\n", exitErr) // TODO: better error handling } log.Println("leaving door handler") return nil @@ -446,11 +465,12 @@ func getUptime(wg *sync.WaitGroup) string { defer wg.Done() out, err := exec.Command("uptime").Output() if err != nil { - return string(err.Error()) // might as well pass on the love to my future co-workers for debugging assistance + return string(err.Error()) // TODO: better error handling } return string(out) } +// returns output of a bash script that collects current wireguard peering connections and their traffic totals func getTubePeers(wg *sync.WaitGroup) string { defer wg.Done() out, err := exec.Command("/usr/local/bin/getTubesPeers.bash").Output() @@ -477,7 +497,7 @@ func getTubePeers(wg *sync.WaitGroup) string { if err != nil { log.Printf("err loading getTubePeers: %v\n", err) - return string(err.Error()) // again, might as well pass on the love to my future co-workers for debugging assistance + return string(err.Error()) // TODO: better error handling } return outStr }