From e6021d6ae7e635341567102d656a81a057fb30dc Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 29 Jul 2018 23:16:09 -0600 Subject: [PATCH] email auth works --- chatserver.go | 152 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 134 insertions(+), 18 deletions(-) diff --git a/chatserver.go b/chatserver.go index ad3e2e2..addbf84 100644 --- a/chatserver.go +++ b/chatserver.go @@ -5,25 +5,32 @@ package main 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" ) +type ConfMailer struct { + Url string `yaml:"url,omitempty"` + ApiKey string `yaml:"api_key,omitempty"` + From string `yaml:"from,omitempty"` +} type Conf struct { Port uint `yaml:"port,omitempty"` - Mailer struct { - ApiKey string `yaml:"api_key,omitempty"` - From string `yaml:"from,omitempty"` - } + Mailer ConfMailer } type bufferedConn struct { @@ -55,10 +62,12 @@ type myMsg struct { sender net.Conn bytes []byte receivedAt time.Time + channel string } var firstMsgs chan myMsg -var myMsgs chan myMsg +var myChans map[string](chan myMsg) +//var myMsgs chan myMsg var myUnsortedConns map[net.Conn]bool var myRawConns map[net.Conn]bool var newConns chan net.Conn @@ -71,6 +80,18 @@ func usage() { 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(conn bufferedConn) { // TODO // What happens if this is being read from range @@ -78,29 +99,72 @@ func handleRaw(conn bufferedConn) { // 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 - buf := make([]byte, 1024) + buffer := make([]byte, 1024) for { fmt.Fprintf(os.Stdout, "[raw] Waiting for message...\n"); - count, err := conn.Read(buf) + count, err := conn.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") + + // TODO put this in a channel to prevent data races + conn.Close(); + delete(myRawConns, conn) 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 { + 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(conn, "Email: ") + continue + } + fmt.Fprintf(os.Stdout, "email: '%v'\n", []byte(email)) + code, err = sendAuthCode(config.Mailer, strings.TrimSpace(email)) + if nil != err { + // TODO handle better + panic(err) + } + fmt.Fprintf(conn, "Auth Code: ") + continue + } + + if code != strings.TrimSpace(string(buf[:count])) { + fmt.Fprintf(conn, "Incorrect Code\nAuth Code: ") + } else { + authn = true + fmt.Fprintf(conn, "Welcome to #general! (TODO `/help' for list of commands)\n") + // TODO number of users + //fmt.Fprintf(conn, "Welcome to #general! TODO `/list' to see channels. `/join chname' to switch.\n") + } continue } + fmt.Fprintf(os.Stdout, "Queing message...\n"); - myMsgs <- myMsg{ + myChans["general"] <- myMsg{ receivedAt: time.Now(), sender: conn, bytes: buf[0:count], + channel: "general", } } } @@ -117,6 +181,7 @@ func handleSorted(conn bufferedConn) { receivedAt: time.Now(), sender: conn, bytes: firstMsg, + channel: "general", } // TODO @@ -142,28 +207,29 @@ func handleSorted(conn bufferedConn) { // fmt.Fprintf(os.Stdout, "Weird") continue } - myMsgs <- myMsg{ + myChans["general"] <- myMsg{ receivedAt: time.Now(), sender: conn, bytes: buf[0:count], + channel: "general", } } } // TODO https://github.com/polvi/sni -func handleConnection(conn net.Conn) { +func handleConnection(netConn net.Conn) { fmt.Fprintf(os.Stdout, "Accepting socket\n") m := sync.Mutex{} virgin := true - myUnsortedConns[conn] = true // Why don't these work? //buf := make([]byte, 0, 1024) //buf := []byte{} // But this does - bufConn := newBufferedConn(conn) + bufConn := newBufferedConn(netConn) + myUnsortedConns[bufConn] = true go func() { // Handle First Packet fmsg, err := bufConn.Peek(1) @@ -177,6 +243,8 @@ func handleConnection(conn net.Conn) { virgin = false go handleSorted(bufConn) } else { + // TODO probably needs to go into a channel + myRawConns[bufConn] = true go handleRaw(bufConn) } m.Unlock(); @@ -188,22 +256,65 @@ func handleConnection(conn net.Conn) { m.Lock() if virgin { virgin = false - // TODO probably needs to go into a channel - myRawConns[conn] = true // don't block for this // let it be handled after the unlock - defer fmt.Fprintf(conn, "Welcome! This is an open relay chat server. There is no security yet.\n") + defer fmt.Fprintf(netConn, "Welcome to Sample Chat! You appear to be using Telnet.\nYou must authenticate via email to participate\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() + 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) + + 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() - var config Conf confstr, err := ioutil.ReadFile(*confname) fmt.Fprintf(os.Stdout, "-conf=%s\n", *confname) if nil != err { @@ -216,11 +327,16 @@ func main() { } firstMsgs = make(chan myMsg, 128) - myMsgs = make(chan myMsg, 128) + //myMsgs = make(chan myMsg, 128) + myChans = make(map[string](chan myMsg)) newConns = make(chan net.Conn, 128) myRawConns = 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) + var addr string if 0 != int(*port) { addr = ":" + strconv.Itoa(int(*port)) @@ -254,7 +370,7 @@ func main() { ts := time.Now() fmt.Fprintf(os.Stdout, "[Handle New Connection] [Timestamp] %s\n", ts) go handleConnection(conn) - case msg := <- myMsgs: + case msg := <- myChans["general"]: ts, err := msg.receivedAt.MarshalJSON() if nil != err { fmt.Fprintf(os.Stderr, "[Error] %s\n", err)