diff --git a/chatserver-http.go b/chatserver-http.go index 3be1040..28f2799 100644 --- a/chatserver-http.go +++ b/chatserver-http.go @@ -13,6 +13,10 @@ import ( restful "github.com/emicklei/go-restful" ) +type JsonMsg struct { + Messages []*chatMsg `json:"messages"` +} + type myHttpServer struct { chans chan bufferedConn net.Listener @@ -43,10 +47,7 @@ type authReq struct { func serveStatic(req *restful.Request, resp *restful.Response) { actual := path.Join(config.RootPath, req.PathParameter("subpath")) fmt.Printf("serving %s ... (from %s)\n", actual, req.PathParameter("subpath")) - http.ServeFile( - resp.ResponseWriter, - req.Request, - actual) + http.ServeFile(resp.ResponseWriter, req.Request, actual) } func serveHello(req *restful.Request, resp *restful.Response) { @@ -93,7 +94,7 @@ func requestAuth(req *restful.Request, resp *restful.Response) { } ar.Otp = otp - // Cheat code in case you didn't set up mailgun keys + // Cheat code in case you didn't set up mailgun keys in the config file fmt.Fprintf(os.Stdout, "\n== HTTP AUTHORIZATION ==\n[cheat code for %s]: %s\n", ar.Subject, ar.Otp) cid, _ := genAuthCode() @@ -102,10 +103,8 @@ func requestAuth(req *restful.Request, resp *restful.Response) { } ar.Cid = cid - newAuthReqs <- ar + authReqs <- ar - // Not sure why this works... technically there needs to be some sort of "end" - // maybe it just figures that if I've returned fmt.Fprintf(resp, "{ \"success\": true, \"cid\": \""+ar.Cid+"\" }") } @@ -118,7 +117,6 @@ func issueToken(req *restful.Request, resp *restful.Response) { return } - //err := json.NewDecoder(r.Body).Decode(&ar) err := req.ReadEntity(&ar) if nil != err { fmt.Fprintf(resp, "{ \"error\": { \"message\": \"bad json in request body\"} }") @@ -152,12 +150,12 @@ func issueToken(req *restful.Request, resp *restful.Response) { // to overwrite the original with the updated // (these are copies, not pointers, IIRC) // and it seems like this is how I might write to a DB anyway - newAuthReqs <- av + authReqs <- av return } av.DidAuth = true ar.VerifiedAt = time.Now() - newAuthReqs <- av + authReqs <- av // TODO I would use a JWT, but I need to wrap up this project fmt.Fprintf(resp, "{ \"success\": true, \"token\": \""+ar.Cid+"\" }") @@ -201,9 +199,8 @@ func requireToken(req *restful.Request, resp *restful.Response, chain *restful.F } func listMsgs(req *restful.Request, resp *restful.Response) { - // TODO support ?since= - // Also, data race? the list could be added to while this is iterating? - // For now we'll just let the client sort the list + // TODO support ?since=, but for now we'll just let the client sort the list + // TODO Could this have a data race if the list were added to while this is iterating? resp.WriteEntity(&JsonMsg{ Messages: myChatHist.msgs[:myChatHist.c], }) diff --git a/chatserver-telnet.go b/chatserver-telnet.go index 3e931a4..bf1c814 100644 --- a/chatserver-telnet.go +++ b/chatserver-telnet.go @@ -9,6 +9,13 @@ import ( "time" ) +type telnetUser struct { + bufConn bufferedConn + userCount chan int + email string + newMsg chan string +} + // Trying to keep it slim with just one goroutine per client for each reads and writes. // Initially I was spawning a goroutine per write in the main select, but my guess is that // constantly allocating and cleaning up 4k of memory (or perhaps less these days @@ -26,7 +33,7 @@ func handleTelnetConn(bufConn bufferedConn) { // Handle all subsequent packets buffer := make([]byte, 1024) - var u *tcpUser + var u *telnetUser for { //fmt.Fprintf(os.Stdout, "[raw] Waiting for message...\n") count, err := bufConn.Read(buffer) @@ -136,7 +143,7 @@ func handleTelnetConn(bufConn bufferedConn) { authn = true time.Sleep(150 * time.Millisecond) fmt.Fprintf(bufConn, "\n") - u = &tcpUser{ + u = &telnetUser{ bufConn: bufConn, email: email, userCount: make(chan int, 1), @@ -156,6 +163,7 @@ func handleTelnetConn(bufConn bufferedConn) { time.Sleep(50 * time.Millisecond) fmt.Fprintf(bufConn, "\n") time.Sleep(50 * time.Millisecond) + // It turns out that ANSI characters work in Telnet just fine fmt.Fprintf(bufConn, "\033[1;32m"+"Welcome to #general (%d users)!"+"\033[22;39m", count) time.Sleep(50 * time.Millisecond) fmt.Fprintf(bufConn, "\n") @@ -166,7 +174,8 @@ func handleTelnetConn(bufConn bufferedConn) { fmt.Fprintf(bufConn, "\n") // Would be cool to write a prompt... - // I wonder if I could send the correct ANSI codes for that... + // I wonder if I could send fudge some ANSI codes to keep the prompt + // even when new messages come in, but not overwrite what he user typed... //fmt.Fprintf(bufConn, "\n%s> ", email) go handleTelnetBroadcast(u) @@ -185,7 +194,7 @@ func handleTelnetConn(bufConn bufferedConn) { } // Writes (post Auth) -func handleTelnetBroadcast(u *tcpUser) { +func handleTelnetBroadcast(u *telnetUser) { for { msg, more := <-u.newMsg if !more { diff --git a/chatserver.go b/chatserver.go index b414bf4..0d7afdf 100644 --- a/chatserver.go +++ b/chatserver.go @@ -39,13 +39,6 @@ type ConfMailer struct { From string `yaml:"from,omitempty"` } -type tcpUser struct { - bufConn bufferedConn - userCount chan int - email string - newMsg chan string -} - // So we can peek at net.Conn, which we can't do natively // https://stackoverflow.com/questions/51472020/how-to-get-the-size-of-available-tcp-data type bufferedConn struct { @@ -80,28 +73,29 @@ type chatMsg struct { Channel string `json:"channel"` User string `json:"user"` } -type JsonMsg struct { - Messages []*chatMsg `json:"messages"` -} +// Poor-Man's container/ring (circular buffer) type chatHist struct { msgs []*chatMsg - i int - c int + i int // current index + c int // current count (number of elements) } +// Multi-use +var config Conf +var virginConns chan net.Conn +var gotClientHello chan bufferedConn var myChatHist chatHist var broadcastMsg chan chatMsg -var virginConns chan net.Conn +// Telnet var wantsServerHello chan bufferedConn -var authTelnet chan tcpUser -var cleanTelnet chan tcpUser -var gotClientHello chan bufferedConn +var authTelnet chan telnetUser +var cleanTelnet chan telnetUser // HTTP var demuxHttpClient chan bufferedConn -var newAuthReqs chan authReq +var authChallenge chan authReq var valAuthReqs chan authReq var delAuthReqs chan authReq @@ -184,7 +178,7 @@ func muxTcp(conn bufferedConn) { demuxHttpClient <- conn } -func handleConnection(netConn net.Conn) { +func testForHello(netConn net.Conn) { ts := time.Now() fmt.Fprintf(os.Stdout, "[New Connection] (%s) welcome %s\n", ts, netConn.RemoteAddr().String()) @@ -193,13 +187,11 @@ func handleConnection(netConn net.Conn) { bufConn := newBufferedConn(netConn) go func() { - // Handle First Packet + // Cause first packet to be loaded into buffer _, err := bufConn.Peek(1) - //fmsg, err := bufConn.Peek(1) if nil != err { panic(err) } - //fmt.Fprintf(os.Stdout, "[First Byte] %s\n", fmsg) m.Lock() if virgin { @@ -211,14 +203,17 @@ func handleConnection(netConn net.Conn) { m.Unlock() }() + // Wait for a hello packet of some sort from the client + // (obviously this wouldn't work in extremely high latency situations) time.Sleep(250 * 1000000) + // If we still haven't received data from the client // assume that the client must be expecting a welcome from us m.Lock() if virgin { virgin = false - // don't block for this - // let it be handled after the unlock + // Defer as to not block and prolonging the mutex + // (not that those few cycles much matter...) defer fmt.Fprintf(netConn, "\n\nWelcome to Sample Chat! You appear to be using Telnet (http is also available on this port)."+ "\nYou must authenticate via email to participate\n\nEmail: ") @@ -251,7 +246,7 @@ func sendAuthCode(cnf ConfMailer, to string) (string, error) { if nil != err { return "", err } - //req.PostForm = form + //req.PostForm = form ?? req.Header.Add("User-Agent", "golang http.Client - Sample Chat App Authenticator") req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth("api", cnf.ApiKey) @@ -276,8 +271,6 @@ func sendAuthCode(cnf ConfMailer, to string) (string, error) { return code, nil } -var config Conf - func main() { flag.Usage = usage port := flag.Uint("port", 0, "tcp telnet chat port") @@ -295,7 +288,8 @@ func main() { config = Conf{} } if "" == config.RootPath { - // TODO Embed the public dir at the default + // TODO Maybe embed the public dir into the binary + // (and provide a flag with path for override - like gitea) config.RootPath = "./public" } @@ -303,28 +297,24 @@ func main() { virginConns = make(chan net.Conn, 128) // TCP & Authentication - telnetConns := make(map[bufferedConn]tcpUser) + telnetConns := make(map[bufferedConn]telnetUser) wantsServerHello = make(chan bufferedConn, 128) - authTelnet = make(chan tcpUser, 128) + authTelnet = make(chan telnetUser, 128) // HTTP & Authentication myAuthReqs := make(map[string]authReq) - newAuthReqs = make(chan authReq, 128) + authReqs = make(chan authReq, 128) valAuthReqs = make(chan authReq, 128) delAuthReqs = make(chan authReq, 128) gotClientHello = make(chan bufferedConn, 128) demuxHttpClient = make(chan bufferedConn, 128) - // cruft to delete - //myRooms = make(map[string](chan chatMsg)) - //myRooms["general"] = make(chan chatMsg, 128) // Note: I had considered dynamically select on channels for rooms. // https://stackoverflow.com/questions/19992334/how-to-listen-to-n-channels-dynamic-select-statement // I don't think that's actually the best approach, but I just wanted to save the link broadcastMsg = make(chan chatMsg, 128) - // Poor-Man's container/ring (circular buffer) myChatHist.msgs = make([]*chatMsg, 128) var addr string @@ -391,7 +381,7 @@ func main() { select { case conn := <-virginConns: // This is short lived - go handleConnection(conn) + go testForHello(conn) case u := <-authTelnet: // allow to receive messages // (and be counted among the users) @@ -405,7 +395,7 @@ func main() { Channel: "general", User: "system", } - case ar := <-newAuthReqs: + case ar := <-authReqs: myAuthReqs[ar.Cid] = ar case ar := <-valAuthReqs: // TODO In this case it's probably more conventional (and efficient) to @@ -455,7 +445,7 @@ func main() { sender = "system" } // Tangential thought: - // I wonder if we could use IP detection to get the client's tz + // I wonder if we could use IP detection to get a Telnet client's tz // ... could probably make time for this in the authentication loop zone, _ := msg.ReceivedAt.Zone() fmt.Fprintf(os.Stdout, tf+" [%s] (%s): %s\r\n", @@ -484,23 +474,6 @@ func main() { // It can reconnect. cleanTelnet <- u } - - /* - // To ask: Why do I have to pass in conn to prevent a data race? Is it garbage collection? - // Don't block the rest of the loop - // TODONE maybe use a chan to send to the socket's event loop - // (left this in to remind myself to ask questions) - go func(conn bufferedConn) { - // Protect against malicious clients to prevent DoS - // https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/ - timeoutDuration := 2 * time.Second - conn.SetWriteDeadline(time.Now().Add(timeoutDuration)) - _, err := fmt.Fprintf(conn, msg) - if nil != err { - cleanTelnet <- u - } - }(conn) - */ } } }