package main // TODO learn more about chan chan's // http://marcio.io/2015/07/handling-1-million-requests-per-minute-with-golang/ import ( "bufio" "bytes" "crypto/rand" "crypto/subtle" "encoding/base64" "flag" "fmt" "io" "io/ioutil" "net" "net/http" "net/url" "os" "path" "strconv" "strings" "sync" "time" "github.com/emicklei/go-restful" "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 RootPath string `yaml:"root_path,omitempty"` } 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 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 { r *bufio.Reader rout io.Reader // See https://github.com/polvi/sni/blob/master/sni.go#L135 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 Message string `json:"message"` ReceivedAt time.Time `json:"received_at"` Channel string `json:"channel"` User string `json:"user"` } type JsonMsg struct { Messages []*myMsg `json:"messages"` } type chatHist struct { msgs []*myMsg i int c int } var myChatHist chatHist var broadcastMsg chan myMsg var newConns chan net.Conn var wantsServerHello chan bufferedConn var authTcpChat chan tcpUser var delTcpChat chan tcpUser var gotClientHello chan bufferedConn var demuxHttpClient chan bufferedConn var delHttpChat chan bufferedConn var newAuthReqs chan authReq var valAuthReqs chan authReq var delAuthReqs chan authReq 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 } // Trying to keep it slim with just one goroutine per client for reads and one goroutine per client for writes. // Initially I was spawning a goroutine per write, but my guess is that constantly allocating and cleaning up 4k // of memory (or perhaps less these days https://blog.nindalf.com/posts/how-goroutines-work/) is probably not // very efficient for small tweet-sized network writes // Auth & Reads func handleTelnetConn(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) var u *tcpUser 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") if nil != u { delTcpChat <- *u } break } buf := buffer[:count] // Rate Limit: Reasonable poor man's DoS prevention (Part 1) // A human does not send messages super fast and blocking the // writes of other incoming messages to this client for this long // won't hinder the user experience (and may in fact enhance it) // TODO: should do this for HTTP as well (or, better yet, implement hashcash) time.Sleep(150 * time.Millisecond) // 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, "[SANITY FAIL] using a 0-length buffer") 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 wg := sync.WaitGroup{} wg.Add(1) go func() { time.Sleep(50 * time.Millisecond) const msg = "Mailing auth code..." for _, r := range msg { time.Sleep(20 * time.Millisecond) fmt.Fprintf(bufConn, string(r)) } time.Sleep(50 * time.Millisecond) 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== TELNET AUTHORIZATION ==\n[cheat code for %s]: %s\n", email, code) time.Sleep(150 * time.Millisecond) fmt.Fprintf(bufConn, " done\n") time.Sleep(150 * time.Millisecond) 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 * time.Millisecond) fmt.Fprintf(bufConn, "\n") u = &tcpUser{ bufConn: bufConn, email: email, userCount: make(chan int, 1), newMsg: make(chan string, 10), // reasonably sized } authTcpChat <- *u // prevent data race on len(myRawConns) // XXX (there can't be a race between these two lines, right?) count := <-u.userCount close(u.userCount) u.userCount = nil // Note: There's a 500ms gap between when we accept the client // and when it can start receiving messages and when it begins // to handle them, however, it's unlikely that >= 10 messages // will simultaneously flood in during that time time.Sleep(50 * time.Millisecond) fmt.Fprintf(bufConn, "\n") time.Sleep(50 * time.Millisecond) fmt.Fprintf(bufConn, "\033[1;32m"+"Welcome to #general (%d users)!"+"\033[22;39m", count) time.Sleep(50 * time.Millisecond) fmt.Fprintf(bufConn, "\n") time.Sleep(50 * time.Millisecond) // TODO /help /join /users /channels /block /upgrade //fmt.Fprintf(bufConn, "(TODO `/help' for list of commands)") time.Sleep(100 * time.Millisecond) fmt.Fprintf(bufConn, "\n") // Would be cool to write a prompt... // I wonder if I could send the correct ANSI codes for that... //fmt.Fprintf(bufConn, "\n%s> ", email) go handleTelnetBroadcast(u) } continue } //fmt.Fprintf(os.Stdout, "Queing message...\n") //myRooms["general"] <- myMsg{ broadcastMsg <- myMsg{ ReceivedAt: time.Now(), sender: bufConn, Message: string(buf[0:count]), Channel: "general", User: email, } //fmt.Fprintf(bufConn, "> ") } } // Writes (post Auth) func handleTelnetBroadcast(u *tcpUser) { for { msg := <-u.newMsg // Disallow Reverse Rate Limit: Reasonable poor man's DoS prevention (Part 3) // https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/ timeoutDuration := 2 * time.Second u.bufConn.SetWriteDeadline(time.Now().Add(timeoutDuration)) _, err := fmt.Fprintf(u.bufConn, msg) if nil != err { delTcpChat <- *u break } } } func muxTcp(conn bufferedConn) { // Wish List for protocol detection // * PROXY protocol (and loop) // * HTTP CONNECT (proxy) (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 // However, typical MTU is 1,500 and HTTP can have a 2k URL // so expecting to find the "HTTP/1.1" in the Peek is not always reasonable n := conn.Buffered() firstMsg, err := conn.Peek(n) if nil != err { conn.Close() return } var protocol string // between A and z if firstMsg[0] >= 65 && firstMsg[0] <= 122 { i := bytes.Index(firstMsg, []byte(" /")) if -1 != i { protocol = "HTTP" // very likely HTTP j := bytes.IndexAny(firstMsg, "\r\n") if -1 != j { k := bytes.Index(bytes.ToLower(firstMsg[:j]), []byte("HTTP/1")) if -1 != k { // positively HTTP } } } } else if 0x16 /*22*/ == firstMsg[0] { // Because I don't always remember off the top of my head what the first byte is // http://blog.fourthbit.com/2014/12/23/traffic-analysis-of-an-ssl-slash-tls-session // https://tlseminar.github.io/first-few-milliseconds/ // TODO I want to learn about ALPN protocol = "TLS" } if "" == protocol { fmt.Fprintf(conn, "\n\nWelcome to Sample Chat! You're not an HTTP client, assuming Telnet.\nYou must authenticate via email to participate\n\nEmail: ") wantsServerHello <- conn return } else if "HTTP" != protocol { defer conn.Close() fmt.Fprintf(conn, "\n\nNot yet supported. Try HTTP or Telnet\n\n") return } demuxHttpClient <- conn } 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 bufConn := newBufferedConn(netConn) 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 gotClientHello <- bufConn } else { wantsServerHello <- 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 (http is also available on this port)."+ "\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 } type myHttpServer struct { chans chan bufferedConn net.Listener } func (m *myHttpServer) Accept() (net.Conn, error) { bufConn := <-m.chans return bufConn, nil } func newHttpServer(l net.Listener) *myHttpServer { return &myHttpServer{make(chan bufferedConn), l} } var config Conf 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) } func serveHello(req *restful.Request, resp *restful.Response) { fmt.Fprintf(resp, "{\"msg\":\"hello\"}") } // TODO I probably should just make the non-exportable properties private/lowercase type authReq struct { Cid string `json:"cid"` ChallengedAt time.Time `json:"-"` Chan chan authReq `json:"-"` Otp string `json:"otp"` CreatedAt time.Time `json:"-"` DidAuth bool `json:"-"` Subject string `json:"sub"` // Subject as in 'sub' as per OIDC VerifiedAt time.Time `json:"-"` Tries int `json:"-"` } func requestAuth(req *restful.Request, resp *restful.Response) { ar := authReq{ CreatedAt: time.Now(), DidAuth: false, Tries: 0, } // Not sure why go restful finds it easier to do ReadEntity() than the "normal" way... // err := json.NewDecoder(req.Body).Decode(&ar) err := req.ReadEntity(&ar) if nil != err { fmt.Fprintf(resp, "{ \"error\": { \"message\": \"bad json in request body\"} }") return } email := strings.TrimSpace(ar.Subject) emailParts := strings.Split(email, "@") // TODO better pre-mailer validation (whitelist characters or use lib) if 2 != len(emailParts) { fmt.Fprintf(resp, "{ \"error\": { \"message\": \"bad email address '"+email+"'\"} }") return } ar.Subject = email var otp string if "" != config.Mailer.ApiKey { otp, err = sendAuthCode(config.Mailer, ar.Subject) if nil != err { fmt.Fprintf(resp, "{ \"error\": { \"message\": \"error sending auth code via mailgun\" } }") return } } if "" == otp { otp, err = genAuthCode() if nil != err { fmt.Fprintf(resp, "{ \"error\": { \"message\": \"error generating random number (code)\"} }") return } } ar.Otp = otp // Cheat code in case you didn't set up mailgun keys fmt.Fprintf(os.Stdout, "\n== HTTP AUTHORIZATION ==\n[cheat code for %s]: %s\n", ar.Subject, ar.Otp) cid, _ := genAuthCode() if "" == cid { fmt.Fprintf(resp, "{ \"error\": { \"message\": \"error generating random number (cid)\"} }") } ar.Cid = cid newAuthReqs <- 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+"\" }") } func issueToken(req *restful.Request, resp *restful.Response) { ar := authReq{} cid := req.PathParameter("cid") if "" == cid { fmt.Fprintf(resp, "{ \"error\": { \"message\": \"bad cid in request url params\"} }") 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\"} }") return } ar.Cid = cid ar.Chan = make(chan authReq) valAuthReqs <- ar av := <-ar.Chan close(ar.Chan) ar.Chan = nil // TODO use a pointer instead? if "" == av.Otp { fmt.Fprintf(resp, "{ \"error\": { \"message\": \"invalid request: empty authorization challenge\"} }") return } av.Tries += 1 av.ChallengedAt = time.Now() // TODO security checks // * ChallengedAt was at least 1 second ago // * Tries does not exceed 5 // * CreatedAt is not more than 15 minutes old // Probably also need to make sure than not more than n emails are sent per y minutes // Not that this would even matter if the above were implemented, just a habit if 1 != subtle.ConstantTimeCompare([]byte(av.Otp), []byte(ar.Otp)) { fmt.Fprintf(resp, "{ \"error\": { \"message\": \"invalid authorization code\"} }") // I'm not sure if this is necessary, but I think it is // 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 return } av.DidAuth = true ar.VerifiedAt = time.Now() newAuthReqs <- av // TODO I would use a JWT, but I need to wrap up this project fmt.Fprintf(resp, "{ \"success\": true, \"token\": \""+ar.Cid+"\" }") } func requireToken(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) { ar := authReq{} auth := req.HeaderParameter("Authorization") if "" == auth { fmt.Fprintf(resp, "{ \"error\": { \"message\": \"missing Authorization header\"} }") return } authParts := strings.Split(auth, " ") if "bearer" != strings.ToLower(authParts[0]) || "" == authParts[1] { fmt.Fprintf(resp, "{ \"error\": { \"message\": \"expected 'Authorization: Bearer '\"} }") return } ar.Cid = authParts[1] ar.Chan = make(chan authReq) valAuthReqs <- ar av := <-ar.Chan close(ar.Chan) ar.Chan = nil // TODO use a pointer instead? if "" == av.Cid { fmt.Fprintf(resp, "{ \"error\": { \"message\": \"invalid token: no session found\"} }") return } // I prefer testing for "if not good" to "if bad" // (much safer in the dynamic world I come from) if true != av.DidAuth { fmt.Fprintf(resp, "{ \"error\": { \"message\": \"bad session'\"} }") return } req.SetAttribute("user", av.Subject) chain.ProcessFilter(req, resp) } 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 resp.WriteEntity(&JsonMsg{ Messages: myChatHist.msgs[:myChatHist.c], }) } func postMsg(req *restful.Request, resp *restful.Response) { user, ok := req.Attribute("user").(string) if !ok { fmt.Fprintf(resp, "{ \"error\": { \"code\": \"E_SANITY\", \"message\": \"SANITY FAIL user was not set, nor session error sent\"} }") return } if "" == user { fmt.Fprintf(resp, "{ \"error\": { \"code\": \"E_SESSION\", \"message\": \"invalid session\"} }") return } msg := myMsg{} err := req.ReadEntity(&msg) if nil != err { fmt.Fprintf(resp, "{ \"error\": { \"code\": \"E_FORMAT\", \"message\": \"invalid json POST\"} }") return } msg.sender = nil msg.ReceivedAt = time.Now() msg.User = user if "" == msg.Channel { msg.Channel = "general" } if "" == msg.Message { fmt.Fprintf(resp, "{ \"error\": { \"code\": \"E_FORMAT\", \"message\": \"please specify a 'message'\"} }") return } broadcastMsg <- msg fmt.Fprintf(resp, "{ \"success\": true }") } 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{} } if "" == config.RootPath { // TODO Embed the public dir at the default config.RootPath = "./public" } // The magical sorting hat newConns = make(chan net.Conn, 128) // TCP & Authentication myRawConns := make(map[bufferedConn]tcpUser) wantsServerHello = make(chan bufferedConn, 128) authTcpChat = make(chan tcpUser, 128) // HTTP & Authentication myAuthReqs := make(map[string]authReq) newAuthReqs = 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 myMsg)) //myRooms["general"] = make(chan myMsg, 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 myMsg, 128) // Poor-Man's container/ring (circular buffer) myChatHist.msgs = make([]*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 } }() // Learning by Example // https://github.com/emicklei/go-restful/blob/master/examples/restful-multi-containers.go // https://github.com/emicklei/go-restful/blob/master/examples/restful-basic-authentication.go // https://github.com/emicklei/go-restful/blob/master/examples/restful-serve-static.go // https://github.com/emicklei/go-restful/blob/master/examples/restful-pre-post-filters.go container := restful.NewContainer() wsStatic := new(restful.WebService) wsStatic.Path("/") wsStatic.Route(wsStatic.GET("/").To(serveStatic)) wsStatic.Route(wsStatic.GET("/{subpath:*}").To(serveStatic)) container.Add(wsStatic) cors := restful.CrossOriginResourceSharing{ExposeHeaders: []string{"Authorization"}, CookiesAllowed: false, Container: container} wsApi := new(restful.WebService) wsApi.Path("/api").Consumes(restful.MIME_JSON).Produces(restful.MIME_JSON).Filter(cors.Filter) wsApi.Route(wsApi.GET("/hello").To(serveHello)) wsApi.Route(wsApi.POST("/sessions").To(requestAuth)) wsApi.Route(wsApi.POST("/sessions/{cid}").To(issueToken)) wsApi.Route(wsApi.GET("/rooms/general").Filter(requireToken).To(listMsgs)) wsApi.Route(wsApi.POST("/rooms/general").Filter(requireToken).To(postMsg)) container.Add(wsApi) server := &http.Server{ Addr: addr, Handler: container, } myHttpServer := newHttpServer(sock) go func() { server.Serve(myHttpServer) }() // 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] = u // is chan chan the right way to handle this? u.userCount <- len(myRawConns) broadcastMsg <- myMsg{ sender: nil, // TODO fmt.Fprintf()? template? Message: "<" + u.email + "> joined #general\n", ReceivedAt: time.Now(), Channel: "general", User: "system", } case ar := <-newAuthReqs: myAuthReqs[ar.Cid] = ar case ar := <-valAuthReqs: // TODO In this case it's probably more conventional (and efficient) to // use struct with a mutex and the authReqs map than a chan chan av, ok := myAuthReqs[ar.Cid] //ar.Chan <- nil // TODO if ok { ar.Chan <- av } else { ar.Chan <- authReq{} } case ar := <-delAuthReqs: delete(myAuthReqs, ar.Cid) case bufConn := <-wantsServerHello: go handleTelnetConn(bufConn) case u := <-delTcpChat: // we can safely ignore this error, if any u.bufConn.Close() delete(myRawConns, u.bufConn) case bufConn := <-gotClientHello: go muxTcp(bufConn) case bufConn := <-demuxHttpClient: // this will be Accept()ed immediately by the go-restful container // NOTE: we don't store these HTTP connections for broadcast // since we manage the session by HTTP Auth Bearer rather than TCP myHttpServer.chans <- bufConn case msg := <-broadcastMsg: // copy comes in, pointer gets saved (and not GC'd, I hope) myChatHist.msgs[myChatHist.i] = &msg myChatHist.i += 1 if myChatHist.c < cap(myChatHist.msgs) { myChatHist.c += 1 } myChatHist.i %= len(myChatHist.msgs) // print the system message (the "log") 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" } // Tangential thought: // 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() 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.User, msg.Message) for _, u := range myRawConns { // Don't echo back to the original client if msg.sender == u.bufConn { continue } msg := fmt.Sprintf(tf+" [%s]: %s", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), zone, msg.User, msg.Message) select { case u.newMsg <- msg: // all is well, client was ready to receive default: // Rate Limit: Reasonable poor man's DoS prevention (Part 2) // This client's send channel buffer is full. // It is consuming data too slowly. It may be malicious. // In the case that it's experiencing network issues, // well, these things happen when you're having network issues. // It can reconnect. delTcpChat <- 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 { delTcpChat <- u } }(conn) */ } } } }