package main // TODO learn about chan chan's // http://marcio.io/2015/07/handling-1-million-requests-per-minute-with-golang/ import ( "bufio" "crypto/rand" "encoding/base64" "flag" "fmt" "io" "io/ioutil" "net" "net/http" "net/url" "os" "strconv" "strings" "sync" "time" "gopkg.in/yaml.v2" ) // I'm not sure how to pass nested structs, so I de-nested this. // TODO: Learn if passing nested structs is desirable? type Conf struct { Port uint `yaml:"port,omitempty"` Mailer ConfMailer } type ConfMailer struct { Url string `yaml:"url,omitempty"` ApiKey string `yaml:"api_key,omitempty"` From string `yaml:"from,omitempty"` } 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 { r *bufio.Reader rout io.Reader net.Conn } func newBufferedConn(c net.Conn) bufferedConn { return bufferedConn{bufio.NewReader(c), nil, c} } func (b bufferedConn) Peek(n int) ([]byte, error) { return b.r.Peek(n) } func (b bufferedConn) Buffered() (int) { return b.r.Buffered() } func (b bufferedConn) Read(p []byte) (int, error) { if b.rout != nil { return b.rout.Read(p) } return b.r.Read(p) } // Just making these all globals right now // because... I can clean it up later type myMsg struct { sender net.Conn bytes []byte receivedAt time.Time channel string email string } var firstMsgs chan myMsg //var myRooms map[string](chan myMsg) var myMsgs chan myMsg //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 func usage() { fmt.Fprintf(os.Stderr, "\nusage: go run chatserver.go\n") flag.PrintDefaults(); fmt.Println() os.Exit(1) } // https://blog.questionable.services/article/generating-secure-random-numbers-crypto-rand/ func genAuthCode() (string, error) { n := 12 b := make([]byte, n) _, err := rand.Read(b) // Note that err == nil only if we read len(b) bytes. if err != nil { return "", err } return base64.URLEncoding.EncodeToString(b), nil } func handleRaw(bufConn bufferedConn) { // TODO // What happens if this is being read from range // when it's being added here (data race)? // Should I use a channel here instead? // TODO see https://jameshfisher.com/2017/04/18/golang-tcp-server.html var email string var code string var authn bool // Handle all subsequent packets buffer := make([]byte, 1024) for { //fmt.Fprintf(os.Stdout, "[raw] Waiting for message...\n"); count, err := bufConn.Read(buffer) if nil != err { if io.EOF != err { fmt.Fprintf(os.Stderr, "Non-EOF socket error: %s\n", err) } fmt.Fprintf(os.Stdout, "Ending socket\n") delTcpChat <- bufConn break } buf := buffer[:count] // Fun fact: if the buffer's current length (not capacity) is 0 // then the Read returns 0 without error if 0 == count { fmt.Fprintf(os.Stdout, "Weird") break } if !authn { if "" == email { // 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, "@") if 2 != len(emailParts) { fmt.Fprintf(bufConn, "Email: ") continue } // 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 } if code != strings.TrimSpace(string(buf[:count])) { fmt.Fprintf(bufConn, "Incorrect Code\nAuth Code: ") } else { authn = true 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"); //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) { // 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 { panic(err) } firstMsgs <- myMsg{ receivedAt: time.Now(), sender: conn, bytes: firstMsg, channel: "general", } // TODO // * TCP-CHAT // * HTTP // * TLS // Handle all subsequent packets buf := make([]byte, 1024) for { fmt.Fprintf(os.Stdout, "[sortable] Waiting for message...\n"); count, err := conn.Read(buf) if nil != err { if io.EOF != err { fmt.Fprintf(os.Stderr, "Non-EOF socket error: %s\n", err) } fmt.Fprintf(os.Stdout, "Ending socket\n") break } // Fun fact: if the buffer's current length (not capacity) is 0 // then the Read returns 0 without error if 0 == count { // fmt.Fprintf(os.Stdout, "Weird") continue } //myRooms["general"] <- myMsg{ myMsgs <- myMsg{ receivedAt: time.Now(), sender: conn, bytes: buf[0:count], channel: "general", } } } func handleConnection(netConn net.Conn) { ts := time.Now() fmt.Fprintf(os.Stdout, "[New Connection] (%s) welcome %s\n", ts, netConn.RemoteAddr().String()) m := sync.Mutex{} virgin := true // Why don't these work? //buf := make([]byte, 0, 1024) //buf := []byte{} // But this does bufConn := newBufferedConn(netConn) //myUnsortedConns[bufConn] = true go func() { // Handle First Packet _, 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 { virgin = false newHttpChat <- bufConn } else { // TODO probably needs to go into a channel newTcpChat <- bufConn } m.Unlock(); }() 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 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() } func sendAuthCode(cnf ConfMailer, to string) (string, error) { code, err := genAuthCode() if nil != err { return "", err } // TODO use go text templates with HTML escaping text := "Your authorization code:\n\n" + code html := "Your authorization code:

" + code // https://stackoverflow.com/questions/24493116/how-to-send-a-post-request-in-go // https://stackoverflow.com/questions/16673766/basic-http-auth-in-go client := http.Client{} form := url.Values{} form.Add("from", cnf.From) form.Add("to", to) form.Add("subject", "Sample Chat Auth Code: " + code) form.Add("text", text) form.Add("html", html) req, err := http.NewRequest("POST", cnf.Url, strings.NewReader(form.Encode())) if nil != err { return "", err } //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) resp, err := client.Do(req) if nil != err { return "", err } 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 } 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 } var config Conf func main() { flag.Usage = usage port := flag.Uint("telnet-port", 0, "tcp telnet chat port") confname := flag.String("conf", "./config.yml", "yaml config file") flag.Parse() confstr, err := ioutil.ReadFile(*confname) fmt.Fprintf(os.Stdout, "-conf=%s\n", *confname) if nil != err { fmt.Fprintf(os.Stderr, "%s\nUsing defaults instead\n", err) confstr = []byte("{\"port\":" + strconv.Itoa(int(*port)) + "}") } err = yaml.Unmarshal(confstr, &config) if nil != err { config = Conf{} } myRawConns := make(map[bufferedConn]bool) firstMsgs = make(chan myMsg, 128) //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) //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 //myRooms["general"] = make(chan myMsg, 128) myMsgs = make(chan myMsg, 128) var addr string if 0 != int(*port) { addr = ":" + strconv.Itoa(int(*port)) } else { addr = ":" + strconv.Itoa(int(config.Port)) } // https://golang.org/pkg/net/#Conn sock, err := net.Listen("tcp", addr) if nil != err { fmt.Fprintf(os.Stderr, "Couldn't bind to TCP socket %q: %s\n", addr, err) os.Exit(2) } fmt.Println("Listening on", addr); go func() { for { conn, err := sock.Accept() if err != nil { // Not sure what kind of error this could be or how it could happen. // Could a connection abort or end before it's handled? fmt.Fprintf(os.Stderr, "Error accepting connection:\n%s\n", err) } newConns <- conn } }() // Main event loop handling most access to shared data for { select { case conn := <- newConns: // 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: go handleRaw(bufConn) case bufConn := <- delTcpChat: // we can safely ignore this error bufConn.Close() delete(myRawConns, bufConn) case bufConn := <- newHttpChat: go handleSorted(bufConn) //case msg := <- myRooms["general"]: //delete(myRooms["general"], bufConn) case msg := <- myMsgs: 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" } // 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 } // 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") ts, err := msg.receivedAt.MarshalJSON() if nil != err { fmt.Fprintf(os.Stderr, "f [Error] %s\n", err) } fmt.Fprintf(os.Stdout, "f [Timestamp] %s\n", ts) fmt.Fprintf(os.Stdout, "f [Remote] %s\n", msg.sender.RemoteAddr().String()) fmt.Fprintf(os.Stdout, "f [Message] %s\n", msg.bytes); } } }