From 9c507ae68ec3e363e2e74bf11eabac127d1d4415 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 30 Jul 2018 23:51:46 -0600 Subject: [PATCH] fix func by passing conn, go fmt (oops), start http detection --- chatserver.go | 948 ++++++++++++++++++++++++++------------------------ 1 file changed, 501 insertions(+), 447 deletions(-) diff --git a/chatserver.go b/chatserver.go index 1106d08..87ee671 100644 --- a/chatserver.go +++ b/chatserver.go @@ -4,42 +4,42 @@ package main // 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" + "bufio" + "bytes" + "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 + 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"` + 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 + bufConn bufferedConn + userCount chan int + email string } // So we can peek at net.Conn, which we can't do natively @@ -58,7 +58,7 @@ func (b bufferedConn) Peek(n int) ([]byte, error) { return b.r.Peek(n) } -func (b bufferedConn) Buffered() (int) { +func (b bufferedConn) Buffered() int { return b.r.Buffered() } @@ -72,38 +72,41 @@ func (b bufferedConn) Read(p []byte) (int, error) { // 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 + 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 newHttpClient chan bufferedConn var delHttpChat chan bufferedConn func usage() { - fmt.Fprintf(os.Stderr, "\nusage: go run chatserver.go\n") - flag.PrintDefaults(); - fmt.Println() + fmt.Fprintf(os.Stderr, "\nusage: go run chatserver.go\n") + flag.PrintDefaults() + fmt.Println() - os.Exit(1) + os.Exit(1) } // https://blog.questionable.services/article/generating-secure-random-numbers-crypto-rand/ func genAuthCode() (string, error) { - n := 12 + n := 12 b := make([]byte, n) _, err := rand.Read(b) - // Note that err == nil only if we read len(b) bytes. + // Note that err == nil only if we read len(b) bytes. if err != nil { return "", err } @@ -111,448 +114,499 @@ func genAuthCode() (string, error) { } 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, "> ") - } + // 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 * 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== 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), + } + 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 * time.Millisecond) + fmt.Fprintf(bufConn, "\n") + time.Sleep(50 * time.Millisecond) + fmt.Fprintf(bufConn, "Welcome to #general (%d users)!", 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") + + // 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", - } - } + // 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: ") + newTcpChat <- conn + return + } else if "HTTP" != protocol { + defer conn.Close() + fmt.Fprintf(conn, "\n\nNot yet supported. Try HTTP or Telnet\n\n") + return + } + + newHttpClient <- conn + + /* + 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()) + ts := time.Now() + fmt.Fprintf(os.Stdout, "[New Connection] (%s) welcome %s\n", ts, netConn.RemoteAddr().String()) - m := sync.Mutex{} - virgin := true + m := sync.Mutex{} + virgin := true - // Why don't these work? - //buf := make([]byte, 0, 1024) - //buf := []byte{} - // But this does + // 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() + //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 { + 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 - } + 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 + // 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 + // 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("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 - } + 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 + 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 { + 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) + newHttpClient = 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); - } - } + } + 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 + } + }() + /* + server := &http.Server{ + Addr: addr, + Handler: &myHandler{}, + } + */ + + // 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 bufConn := <-newHttpClient: + //server.Serve(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 + } + + // 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 + // TODO maybe use a chan to send to the socket's event loop + go func(conn bufferedConn) { + // 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 + } + }(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) + } + } }