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)