mostly just cleanup I think...

This commit is contained in:
AJ ONeal 2018-07-30 17:02:21 -06:00
parent 34ee3924d3
commit 64425b41d2
1 changed files with 175 additions and 43 deletions

View File

@ -36,6 +36,12 @@ type ConfMailer struct {
} }
type tcpUser struct {
bufConn bufferedConn
userCount chan int
email string
}
// So we can peek at net.Conn, which we can't do natively // 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 // https://stackoverflow.com/questions/51472020/how-to-get-the-size-of-available-tcp-data
type bufferedConn struct { type bufferedConn struct {
@ -70,15 +76,16 @@ type myMsg struct {
bytes []byte bytes []byte
receivedAt time.Time receivedAt time.Time
channel string channel string
email string
} }
var firstMsgs chan myMsg var firstMsgs chan myMsg
var myChans map[string](chan myMsg) //var myRooms map[string](chan myMsg)
var myMsgs chan myMsg var myMsgs chan myMsg
var myUnsortedConns map[net.Conn]bool //var myUnsortedConns map[net.Conn]bool
var myRawConns map[net.Conn]bool
var newConns chan net.Conn var newConns chan net.Conn
var newTcpChat chan bufferedConn var newTcpChat chan bufferedConn
var authTcpChat chan tcpUser
var delTcpChat chan bufferedConn var delTcpChat chan bufferedConn
var newHttpChat chan bufferedConn var newHttpChat chan bufferedConn
var delHttpChat chan bufferedConn var delHttpChat chan bufferedConn
@ -117,7 +124,7 @@ func handleRaw(bufConn bufferedConn) {
// Handle all subsequent packets // Handle all subsequent packets
buffer := make([]byte, 1024) buffer := make([]byte, 1024)
for { for {
fmt.Fprintf(os.Stdout, "[raw] Waiting for message...\n"); //fmt.Fprintf(os.Stdout, "[raw] Waiting for message...\n");
count, err := bufConn.Read(buffer) count, err := bufConn.Read(buffer)
if nil != err { if nil != err {
if io.EOF != err { if io.EOF != err {
@ -125,7 +132,6 @@ func handleRaw(bufConn bufferedConn) {
} }
fmt.Fprintf(os.Stdout, "Ending socket\n") fmt.Fprintf(os.Stdout, "Ending socket\n")
// TODO put this in a channel to prevent data races
delTcpChat <- bufConn delTcpChat <- bufConn
break break
} }
@ -140,7 +146,9 @@ func handleRaw(bufConn bufferedConn) {
if !authn { if !authn {
if "" == email { if "" == email {
fmt.Fprintf(os.Stdout, "buf{%s}\n", buf[:count]) // Indeed telnet sends CRLF as part of the message
//fmt.Fprintf(os.Stdout, "buf{%s}\n", buf[:count])
// TODO use safer email testing // TODO use safer email testing
email = strings.TrimSpace(string(buf[:count])) email = strings.TrimSpace(string(buf[:count]))
emailParts := strings.Split(email, "@") emailParts := strings.Split(email, "@")
@ -148,12 +156,53 @@ func handleRaw(bufConn bufferedConn) {
fmt.Fprintf(bufConn, "Email: ") fmt.Fprintf(bufConn, "Email: ")
continue continue
} }
fmt.Fprintf(os.Stdout, "email: '%v'\n", []byte(email))
// 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)) code, err = sendAuthCode(config.Mailer, strings.TrimSpace(email))
wg.Done()
}()
} else {
code, err = genAuthCode()
}
wg.Wait()
if nil != err { if nil != err {
// TODO handle better // 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) 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: ") fmt.Fprintf(bufConn, "Auth Code: ")
continue continue
} }
@ -162,27 +211,64 @@ func handleRaw(bufConn bufferedConn) {
fmt.Fprintf(bufConn, "Incorrect Code\nAuth Code: ") fmt.Fprintf(bufConn, "Incorrect Code\nAuth Code: ")
} else { } else {
authn = true authn = true
fmt.Fprintf(bufConn, "Welcome to #general! (TODO `/help' for list of commands)\n") time.Sleep(150 * 1000000)
// TODO number of users fmt.Fprintf(bufConn, "\n")
//fmt.Fprintf(bufConn, "Welcome to #general! TODO `/list' to see channels. `/join chname' to switch.\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 <room> /users /channels /block <user> /upgrade <http/ws>
//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 continue
} }
fmt.Fprintf(os.Stdout, "Queing message...\n"); //fmt.Fprintf(os.Stdout, "Queing message...\n");
//myChans["general"] <- myMsg{ //myRooms["general"] <- myMsg{
myMsgs <- myMsg{ myMsgs <- myMsg{
receivedAt: time.Now(), receivedAt: time.Now(),
sender: bufConn, sender: bufConn,
bytes: buf[0:count], bytes: buf[0:count],
channel: "general", channel: "general",
email: email,
} }
//fmt.Fprintf(bufConn, "> ")
} }
} }
func handleSorted(conn bufferedConn) { func handleSorted(conn bufferedConn) {
// at this piont we've already at least one byte via Peek() // 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 // 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() n := conn.Buffered()
firstMsg, err := conn.Peek(n) firstMsg, err := conn.Peek(n)
if nil != err { if nil != err {
@ -218,7 +304,7 @@ func handleSorted(conn bufferedConn) {
// fmt.Fprintf(os.Stdout, "Weird") // fmt.Fprintf(os.Stdout, "Weird")
continue continue
} }
//myChans["general"] <- myMsg{ //myRooms["general"] <- myMsg{
myMsgs <- myMsg{ myMsgs <- myMsg{
receivedAt: time.Now(), receivedAt: time.Now(),
sender: conn, sender: conn,
@ -228,9 +314,9 @@ func handleSorted(conn bufferedConn) {
} }
} }
// TODO https://github.com/polvi/sni
func handleConnection(netConn net.Conn) { func handleConnection(netConn net.Conn) {
fmt.Fprintf(os.Stdout, "Accepting socket\n") ts := time.Now()
fmt.Fprintf(os.Stdout, "[New Connection] (%s) welcome %s\n", ts, netConn.RemoteAddr().String())
m := sync.Mutex{} m := sync.Mutex{}
virgin := true virgin := true
@ -241,14 +327,15 @@ func handleConnection(netConn net.Conn) {
// But this does // But this does
bufConn := newBufferedConn(netConn) bufConn := newBufferedConn(netConn)
myUnsortedConns[bufConn] = true //myUnsortedConns[bufConn] = true
go func() { go func() {
// Handle First Packet // Handle First Packet
fmsg, err := bufConn.Peek(1) _, err := bufConn.Peek(1)
//fmsg, err := bufConn.Peek(1)
if nil != err { if nil != err {
panic(err) panic(err)
} }
fmt.Fprintf(os.Stdout, "[First Byte] %s\n", fmsg) //fmt.Fprintf(os.Stdout, "[First Byte] %s\n", fmsg)
m.Lock(); m.Lock();
if virgin { if virgin {
@ -269,7 +356,7 @@ func handleConnection(netConn net.Conn) {
virgin = false virgin = false
// don't block for this // don't block for this
// let it be handled after the unlock // let it be handled after the unlock
defer fmt.Fprintf(netConn, "Welcome to Sample Chat! You appear to be using Telnet.\nYou must authenticate via email to participate\nEmail: ") 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() m.Unlock()
} }
@ -310,11 +397,18 @@ func sendAuthCode(cnf ConfMailer, to string) (string, error) {
} }
defer resp.Body.Close() 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) body, err := ioutil.ReadAll(resp.Body)
if nil != err { if nil != err {
return "", err return "", err
} }
fmt.Fprintf(os.Stdout, "Here's what Mailgun had to say about the event: %s\n", body) 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 return code, nil
} }
@ -337,17 +431,18 @@ func main() {
config = Conf{} config = Conf{}
} }
myRawConns := make(map[bufferedConn]bool)
firstMsgs = make(chan myMsg, 128) firstMsgs = make(chan myMsg, 128)
myChans = make(map[string](chan myMsg)) //myRooms = make(map[string](chan myMsg))
newConns = make(chan net.Conn, 128) newConns = make(chan net.Conn, 128)
authTcpChat = make(chan tcpUser, 128)
newTcpChat = make(chan bufferedConn, 128) newTcpChat = make(chan bufferedConn, 128)
newHttpChat = make(chan bufferedConn, 128) newHttpChat = make(chan bufferedConn, 128)
myRawConns = make(map[net.Conn]bool) //myUnsortedConns = make(map[net.Conn]bool)
myUnsortedConns = make(map[net.Conn]bool)
// TODO dynamically select on channels? // TODO dynamically select on channels?
// https://stackoverflow.com/questions/19992334/how-to-listen-to-n-channels-dynamic-select-statement // https://stackoverflow.com/questions/19992334/how-to-listen-to-n-channels-dynamic-select-statement
//myChans["general"] = make(chan myMsg, 128) //myRooms["general"] = make(chan myMsg, 128)
myMsgs = make(chan myMsg, 128) myMsgs = make(chan myMsg, 128)
var addr string var addr string
@ -377,40 +472,77 @@ func main() {
} }
}() }()
// Main event loop handling most access to shared data
for { for {
select { select {
case conn := <- newConns: case conn := <- newConns:
ts := time.Now()
fmt.Fprintf(os.Stdout, "[Handle New Connection] [Timestamp] %s\n", ts)
// This is short lived // This is short lived
go handleConnection(conn) 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: case bufConn := <- newTcpChat:
myRawConns[bufConn] = true
go handleRaw(bufConn) go handleRaw(bufConn)
case bufConn := <- delTcpChat: case bufConn := <- delTcpChat:
bufConn.Close(); // we can safely ignore this error
bufConn.Close()
delete(myRawConns, bufConn) delete(myRawConns, bufConn)
case bufConn := <- newHttpChat: case bufConn := <- newHttpChat:
go handleSorted(bufConn) go handleSorted(bufConn)
//case msg := <- myChans["general"]: //case msg := <- myRooms["general"]:
//delete(myChans["general"], bufConn) //delete(myRooms["general"], bufConn)
case msg := <- myMsgs: case msg := <- myMsgs:
ts, err := msg.receivedAt.MarshalJSON() t := msg.receivedAt
if nil != err { tf := "%d-%02d-%02d %02d:%02d:%02d (%s)"
fmt.Fprintf(os.Stderr, "[Error] %s\n", err) var sender string
if nil != msg.sender {
sender = msg.sender.RemoteAddr().String()
} else {
sender = "system"
} }
fmt.Fprintf(os.Stdout, "[Timestamp] %s\n", ts) // I wonder if we could use IP detection to get the client's tz
fmt.Fprintf(os.Stdout, "[Remote] %s\n", msg.sender.RemoteAddr().String()) // ... could probably make time for this in the authentication loop
fmt.Fprintf(os.Stdout, "[Message] %s\n", msg.bytes); 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 { for conn, _ := range myRawConns {
// Don't echo back to the original client
if msg.sender == conn { if msg.sender == conn {
continue continue
} }
// backlogged connections could prevent a next write,
// so this should be refactored into a goroutine // Don't block the rest of the loop
// And what to do about slow clients that get behind (or DoS)? // TODO maybe use a chan to send to the socket's event loop
// SetDeadTime and Disconnect them? go func() {
conn.Write(msg.bytes) // 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: case msg := <- firstMsgs:
fmt.Fprintf(os.Stdout, "f [First Message]\n") fmt.Fprintf(os.Stdout, "f [First Message]\n")