diff --git a/chatserver.go b/chatserver.go index 90f12cc..1106d08 100644 --- a/chatserver.go +++ b/chatserver.go @@ -36,6 +36,12 @@ type ConfMailer struct { } +type tcpUser struct { + bufConn bufferedConn + userCount chan int + email 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 { @@ -70,15 +76,16 @@ type myMsg struct { bytes []byte receivedAt time.Time channel string + email string } var firstMsgs chan myMsg -var myChans map[string](chan myMsg) +//var myRooms map[string](chan myMsg) var myMsgs chan myMsg -var myUnsortedConns map[net.Conn]bool -var myRawConns map[net.Conn]bool +//var myUnsortedConns map[net.Conn]bool var newConns chan net.Conn var newTcpChat chan bufferedConn +var authTcpChat chan tcpUser var delTcpChat chan bufferedConn var newHttpChat chan bufferedConn var delHttpChat chan bufferedConn @@ -117,7 +124,7 @@ func handleRaw(bufConn bufferedConn) { // Handle all subsequent packets buffer := make([]byte, 1024) for { - fmt.Fprintf(os.Stdout, "[raw] Waiting for message...\n"); + //fmt.Fprintf(os.Stdout, "[raw] Waiting for message...\n"); count, err := bufConn.Read(buffer) if nil != err { if io.EOF != err { @@ -125,7 +132,6 @@ func handleRaw(bufConn bufferedConn) { } fmt.Fprintf(os.Stdout, "Ending socket\n") - // TODO put this in a channel to prevent data races delTcpChat <- bufConn break } @@ -140,7 +146,9 @@ func handleRaw(bufConn bufferedConn) { if !authn { if "" == email { - fmt.Fprintf(os.Stdout, "buf{%s}\n", buf[:count]) + // Indeed telnet sends CRLF as part of the message + //fmt.Fprintf(os.Stdout, "buf{%s}\n", buf[:count]) + // TODO use safer email testing email = strings.TrimSpace(string(buf[:count])) emailParts := strings.Split(email, "@") @@ -148,12 +156,53 @@ func handleRaw(bufConn bufferedConn) { fmt.Fprintf(bufConn, "Email: ") continue } - fmt.Fprintf(os.Stdout, "email: '%v'\n", []byte(email)) - code, err = sendAuthCode(config.Mailer, strings.TrimSpace(email)) + + // Debugging any weird characters as part of the message (just CRLF) + //fmt.Fprintf(os.Stdout, "email: '%v'\n", []byte(email)) + + // Just for a fun little bit of puzzah + // Note: Reaction times are about 100ms + // Procesing times are about 250ms + // Right around 300ms is about when a person literally begins to get bored (begin context switching) + // Therefore any interaction should take longer than 100ms (time to register) + // and either engage the user or complete before reaching 300ms (not yet bored) + // This little ditty is meant to act as a psuedo-progress bar to engage the user + // Aside: a keystroke typically takes >=50s to type (probably closer to 200ms between words) + // https://stackoverflow.com/questions/22505698/what-is-a-typical-keypress-duration + var wg sync.WaitGroup + wg.Add(1) + go func() { + time.Sleep(50 * 1000000) + const msg = "Mailing auth code..." + for _, r := range msg { + time.Sleep(20 * 1000000) + fmt.Fprintf(bufConn, string(r)) + } + time.Sleep(50 * 1000000) + wg.Done() + }() + if "" != config.Mailer.ApiKey { + wg.Add(1) + go func() { + code, err = sendAuthCode(config.Mailer, strings.TrimSpace(email)) + wg.Done() + }() + } else { + code, err = genAuthCode() + } + wg.Wait() if nil != err { // TODO handle better + // (not sure why a random number would fail, + // but on a machine without internet the calls + // to mailgun APIs would fail) panic(err) } + // so I don't have to actually go check my email + fmt.Fprintf(os.Stdout, "\n== AUTHORIZATION ==\n[cheat code for %s]: %s\n", email, code) + time.Sleep(150 * 1000000) + fmt.Fprintf(bufConn, " done\n") + time.Sleep(150 * 1000000) fmt.Fprintf(bufConn, "Auth Code: ") continue } @@ -162,27 +211,64 @@ func handleRaw(bufConn bufferedConn) { fmt.Fprintf(bufConn, "Incorrect Code\nAuth Code: ") } else { authn = true - fmt.Fprintf(bufConn, "Welcome to #general! (TODO `/help' for list of commands)\n") - // TODO number of users - //fmt.Fprintf(bufConn, "Welcome to #general! TODO `/list' to see channels. `/join chname' to switch.\n") + time.Sleep(150 * 1000000) + fmt.Fprintf(bufConn, "\n") + u := tcpUser{ + bufConn: bufConn, + email: email, + userCount: make(chan int, 1), + } + authTcpChat <- u + // prevent data race on len(myRawConns) + // XXX (there can't be a race between these two lines, right?) + count := <- u.userCount + u.userCount = nil + time.Sleep(50 * 1000000) + fmt.Fprintf(bufConn, "\n") + time.Sleep(50 * 1000000) + fmt.Fprintf(bufConn, "Welcome to #general (%d users)!", count) + time.Sleep(50 * 1000000) + fmt.Fprintf(bufConn, "\n") + time.Sleep(50 * 1000000) + // TODO /help /join /users /channels /block /upgrade + //fmt.Fprintf(bufConn, "(TODO `/help' for list of commands)") + time.Sleep(100 * 1000000) + fmt.Fprintf(bufConn, "\n") + + // this would be cool, but won't work since other messages will come + // in before the person responds + //fmt.Fprintf(bufConn, "\n%s> ", email) } continue } - fmt.Fprintf(os.Stdout, "Queing message...\n"); - //myChans["general"] <- myMsg{ + //fmt.Fprintf(os.Stdout, "Queing message...\n"); + //myRooms["general"] <- myMsg{ myMsgs <- myMsg{ receivedAt: time.Now(), sender: bufConn, bytes: buf[0:count], channel: "general", + email: email, } + //fmt.Fprintf(bufConn, "> ") } } func handleSorted(conn bufferedConn) { - // at this piont we've already at least one byte via Peek() + // Wish List for protocol detection + // * PROXY protocol (and loop) + // * tls (and loop) https://github.com/polvi/sni + // * http/ws + // * irc + // * fallback to telnet + + // At this piont we've already at least one byte via Peek() // so the first packet is available in the buffer + + // Note: Realistically no tls/http/irc client is going to send so few bytes + // (and no router is going to chunk so small) + // that it cannot reasonably detect the protocol in the first packet n := conn.Buffered() firstMsg, err := conn.Peek(n) if nil != err { @@ -218,7 +304,7 @@ func handleSorted(conn bufferedConn) { // fmt.Fprintf(os.Stdout, "Weird") continue } - //myChans["general"] <- myMsg{ + //myRooms["general"] <- myMsg{ myMsgs <- myMsg{ receivedAt: time.Now(), sender: conn, @@ -228,9 +314,9 @@ func handleSorted(conn bufferedConn) { } } -// TODO https://github.com/polvi/sni func handleConnection(netConn net.Conn) { - fmt.Fprintf(os.Stdout, "Accepting socket\n") + ts := time.Now() + fmt.Fprintf(os.Stdout, "[New Connection] (%s) welcome %s\n", ts, netConn.RemoteAddr().String()) m := sync.Mutex{} virgin := true @@ -241,14 +327,15 @@ func handleConnection(netConn net.Conn) { // But this does bufConn := newBufferedConn(netConn) - myUnsortedConns[bufConn] = true + //myUnsortedConns[bufConn] = true go func() { // Handle First Packet - fmsg, err := bufConn.Peek(1) + _, err := bufConn.Peek(1) + //fmsg, err := bufConn.Peek(1) if nil != err { panic(err) } - fmt.Fprintf(os.Stdout, "[First Byte] %s\n", fmsg) + //fmt.Fprintf(os.Stdout, "[First Byte] %s\n", fmsg) m.Lock(); if virgin { @@ -269,7 +356,7 @@ func handleConnection(netConn net.Conn) { virgin = false // don't block for this // let it be handled after the unlock - defer fmt.Fprintf(netConn, "Welcome to Sample Chat! You appear to be using Telnet.\nYou must authenticate via email to participate\nEmail: ") + defer fmt.Fprintf(netConn, "\n\nWelcome to Sample Chat! You appear to be using Telnet.\nYou must authenticate via email to participate\n\nEmail: ") } m.Unlock() } @@ -310,11 +397,18 @@ func sendAuthCode(cnf ConfMailer, to string) (string, error) { } defer resp.Body.Close() + // Security XXX + // we trust mailgun implicitly and this is just a demo + // hence no DoS check on body size for now body, err := ioutil.ReadAll(resp.Body) if nil != err { return "", err } - fmt.Fprintf(os.Stdout, "Here's what Mailgun had to say about the event: %s\n", body) + if resp.StatusCode < 200 || resp.StatusCode >= 300 || "{" != string(body[0]) { + fmt.Fprintf(os.Stdout, "[Mailgun] Uh-oh...\n[Maigun] Baby Brent says: %s\n", body) + } else { + fmt.Fprintf(os.Stdout, "[Mailgun] Status: %d", resp.StatusCode) + } return code, nil } @@ -337,17 +431,18 @@ func main() { config = Conf{} } + myRawConns := make(map[bufferedConn]bool) firstMsgs = make(chan myMsg, 128) - myChans = make(map[string](chan myMsg)) + //myRooms = make(map[string](chan myMsg)) newConns = make(chan net.Conn, 128) + authTcpChat = make(chan tcpUser, 128) newTcpChat = make(chan bufferedConn, 128) newHttpChat = make(chan bufferedConn, 128) - myRawConns = make(map[net.Conn]bool) - myUnsortedConns = make(map[net.Conn]bool) + //myUnsortedConns = make(map[net.Conn]bool) // TODO dynamically select on channels? // https://stackoverflow.com/questions/19992334/how-to-listen-to-n-channels-dynamic-select-statement - //myChans["general"] = make(chan myMsg, 128) + //myRooms["general"] = make(chan myMsg, 128) myMsgs = make(chan myMsg, 128) var addr string @@ -377,40 +472,77 @@ func main() { } }() + // Main event loop handling most access to shared data for { select { case conn := <- newConns: - ts := time.Now() - fmt.Fprintf(os.Stdout, "[Handle New Connection] [Timestamp] %s\n", ts) // This is short lived go handleConnection(conn) + case u := <- authTcpChat: + // allow to receive messages + // (and be counted among the users) + myRawConns[u.bufConn] = true + // is chan chan the right way to handle this? + u.userCount <- len(myRawConns) + myMsgs <- myMsg{ + sender: nil, + // TODO fmt.Fprintf()? template? + bytes: []byte("<" + u.email + "> joined #general\n"), + receivedAt: time.Now(), + channel: "general", + email: "system", + } case bufConn := <- newTcpChat: - myRawConns[bufConn] = true go handleRaw(bufConn) case bufConn := <- delTcpChat: - bufConn.Close(); + // we can safely ignore this error + bufConn.Close() delete(myRawConns, bufConn) case bufConn := <- newHttpChat: go handleSorted(bufConn) - //case msg := <- myChans["general"]: - //delete(myChans["general"], bufConn) + //case msg := <- myRooms["general"]: + //delete(myRooms["general"], bufConn) case msg := <- myMsgs: - ts, err := msg.receivedAt.MarshalJSON() - if nil != err { - fmt.Fprintf(os.Stderr, "[Error] %s\n", err) + t := msg.receivedAt + tf := "%d-%02d-%02d %02d:%02d:%02d (%s)" + var sender string + if nil != msg.sender { + sender = msg.sender.RemoteAddr().String() + } else { + sender = "system" } - fmt.Fprintf(os.Stdout, "[Timestamp] %s\n", ts) - fmt.Fprintf(os.Stdout, "[Remote] %s\n", msg.sender.RemoteAddr().String()) - fmt.Fprintf(os.Stdout, "[Message] %s\n", msg.bytes); + // I wonder if we could use IP detection to get the client's tz + // ... could probably make time for this in the authentication loop + zone, _ := msg.receivedAt.Zone() + + //ts, err := msg.receivedAt.MarshalJSON() + fmt.Fprintf(os.Stdout, tf + " [%s] (%s):\n\t%s", + t.Year(), t.Month(), t.Day(), + t.Hour(), t.Minute(), t.Second(), zone, + sender, + msg.email, msg.bytes) + for conn, _ := range myRawConns { + // Don't echo back to the original client if msg.sender == conn { continue } - // backlogged connections could prevent a next write, - // so this should be refactored into a goroutine - // And what to do about slow clients that get behind (or DoS)? - // SetDeadTime and Disconnect them? - conn.Write(msg.bytes) + + // Don't block the rest of the loop + // TODO maybe use a chan to send to the socket's event loop + go func() { + // Protect against malicious clients to prevent DoS + // https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/ + timeoutDuration := 5 * time.Second + conn.SetWriteDeadline(time.Now().Add(timeoutDuration)) + _, err := fmt.Fprintf(conn, tf + " [%s]: %s", + t.Year(), t.Month(), t.Day(), + t.Hour(), t.Minute(), t.Second(), zone, + msg.email, msg.bytes) + if nil != err { + delTcpChat <- conn + } + }() } case msg := <- firstMsgs: fmt.Fprintf(os.Stdout, "f [First Message]\n")