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" "encoding/base64" "flag" "fmt" "io/ioutil" "net" "net/http" "net/url" "os" "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 { Addr string `yaml:"addr,omitempty"` 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"` } // 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), 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) } type chatMsg struct { sender net.Conn Message string `json:"message"` ReceivedAt time.Time `json:"received_at"` Channel string `json:"channel"` User string `json:"user"` } // Poor-Man's container/ring (circular buffer) type chatHist struct { msgs []*chatMsg 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 // Telnet var wantsServerHello chan bufferedConn var authTelnet chan telnetUser var cleanTelnet chan telnetUser // intentionally blocking // HTTP var demuxHttpClient chan bufferedConn var authReqs 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 } 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 { // Throw away the first bytes b := make([]byte, 4096) conn.Read(b) 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 testForHello(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() { // Cause first packet to be loaded into buffer _, err := bufConn.Peek(1) if nil != err { panic(err) } m.Lock() if virgin { virgin = false gotClientHello <- bufConn } else { wantsServerHello <- bufConn } 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 // 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: ") } 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) } return code, nil } func main() { flag.Usage = usage port := flag.Uint("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 Maybe embed the public dir into the binary // (and provide a flag with path for override - like gitea) config.RootPath = "./public" } // The magical sorting hat virginConns = make(chan net.Conn, 128) // TCP & Authentication telnetConns := make(map[string]telnetUser) wantsServerHello = make(chan bufferedConn, 128) authTelnet = make(chan telnetUser, 128) // HTTP & Authentication myAuthReqs := make(map[string]authReq) 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) //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) myChatHist.msgs = make([]*chatMsg, 128) var addr string if 0 != int(*port) { addr = config.Addr + ":" + strconv.Itoa(int(*port)) } else { addr = config.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) } virginConns <- 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/{room}").Filter(requireToken).To(listMsgs)) wsApi.Route(wsApi.POST("/rooms/{room}").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 := <-virginConns: // This is short lived go testForHello(conn) case u := <-authTelnet: // allow to receive messages // (and be counted among the users) _, ok := telnetConns[u.email] if ok { // this is a blocking channel, and that's important cleanTelnet <- telnetConns[u.email] } telnetConns[u.email] = u // is chan chan the right way to handle this? u.userCount <- len(telnetConns) broadcastMsg <- chatMsg{ sender: nil, Message: fmt.Sprintf("<%s> joined #general\r\n", u.email), ReceivedAt: time.Now(), Channel: "general", User: "system", } case ar := <-authReqs: 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 { // sending empty object so that I can still send a copy // rather than a pointer above. Maybe not the right way // to do this, but it works for now. ar.Chan <- authReq{} } case ar := <-delAuthReqs: delete(myAuthReqs, ar.Cid) case bufConn := <-wantsServerHello: go handleTelnetConn(bufConn) case u := <-cleanTelnet: close(u.newMsg) // we can safely ignore this error, if any u.bufConn.Close() delete(telnetConns, u.email) 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 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", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), zone, sender, msg.User, msg.Message) for _, u := range telnetConns { // 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. cleanTelnet <- u } } } } }