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
// https://stackoverflow.com/questions/51472020/how-to-get-the-size-of-available-tcp-data
type bufferedConn struct {
@ -70,15 +76,16 @@ type myMsg struct {
bytes []byte
receivedAt time.Time
channel string
email string
}
var firstMsgs chan myMsg
var myChans map[string](chan myMsg)
//var myRooms map[string](chan myMsg)
var myMsgs chan myMsg
var myUnsortedConns map[net.Conn]bool
var myRawConns map[net.Conn]bool
//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 delHttpChat chan bufferedConn
@ -117,7 +124,7 @@ func handleRaw(bufConn bufferedConn) {
// Handle all subsequent packets
buffer := make([]byte, 1024)
for {
fmt.Fprintf(os.Stdout, "[raw] Waiting for message...\n");
//fmt.Fprintf(os.Stdout, "[raw] Waiting for message...\n");
count, err := bufConn.Read(buffer)
if nil != err {
if io.EOF != err {
@ -125,7 +132,6 @@ func handleRaw(bufConn bufferedConn) {
}
fmt.Fprintf(os.Stdout, "Ending socket\n")
// TODO put this in a channel to prevent data races
delTcpChat <- bufConn
break
}
@ -140,7 +146,9 @@ func handleRaw(bufConn bufferedConn) {
if !authn {
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
email = strings.TrimSpace(string(buf[:count]))
emailParts := strings.Split(email, "@")
@ -148,12 +156,53 @@ func handleRaw(bufConn bufferedConn) {
fmt.Fprintf(bufConn, "Email: ")
continue
}
fmt.Fprintf(os.Stdout, "email: '%v'\n", []byte(email))
code, err = sendAuthCode(config.Mailer, strings.TrimSpace(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))
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
}
@ -162,27 +211,64 @@ func handleRaw(bufConn bufferedConn) {
fmt.Fprintf(bufConn, "Incorrect Code\nAuth Code: ")
} else {
authn = true
fmt.Fprintf(bufConn, "Welcome to #general! (TODO `/help' for list of commands)\n")
// TODO number of users
//fmt.Fprintf(bufConn, "Welcome to #general! TODO `/list' to see channels. `/join chname' to switch.\n")
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 <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
}
fmt.Fprintf(os.Stdout, "Queing message...\n");
//myChans["general"] <- myMsg{
//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) {
// 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
// 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 {
@ -218,7 +304,7 @@ func handleSorted(conn bufferedConn) {
// fmt.Fprintf(os.Stdout, "Weird")
continue
}
//myChans["general"] <- myMsg{
//myRooms["general"] <- myMsg{
myMsgs <- myMsg{
receivedAt: time.Now(),
sender: conn,
@ -228,9 +314,9 @@ func handleSorted(conn bufferedConn) {
}
}
// TODO https://github.com/polvi/sni
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{}
virgin := true
@ -241,14 +327,15 @@ func handleConnection(netConn net.Conn) {
// But this does
bufConn := newBufferedConn(netConn)
myUnsortedConns[bufConn] = true
//myUnsortedConns[bufConn] = true
go func() {
// Handle First Packet
fmsg, err := bufConn.Peek(1)
_, err := bufConn.Peek(1)
//fmsg, err := bufConn.Peek(1)
if nil != err {
panic(err)
}
fmt.Fprintf(os.Stdout, "[First Byte] %s\n", fmsg)
//fmt.Fprintf(os.Stdout, "[First Byte] %s\n", fmsg)
m.Lock();
if virgin {
@ -269,7 +356,7 @@ func handleConnection(netConn net.Conn) {
virgin = false
// don't block for this
// 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()
}
@ -310,11 +397,18 @@ func sendAuthCode(cnf ConfMailer, to string) (string, error) {
}
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
}
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
}
@ -337,17 +431,18 @@ func main() {
config = Conf{}
}
myRawConns := make(map[bufferedConn]bool)
firstMsgs = make(chan myMsg, 128)
myChans = make(map[string](chan myMsg))
//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)
myRawConns = make(map[net.Conn]bool)
myUnsortedConns = 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)
//myRooms["general"] = make(chan myMsg, 128)
myMsgs = make(chan myMsg, 128)
var addr string
@ -377,40 +472,77 @@ func main() {
}
}()
// Main event loop handling most access to shared data
for {
select {
case conn := <- newConns:
ts := time.Now()
fmt.Fprintf(os.Stdout, "[Handle New Connection] [Timestamp] %s\n", ts)
// 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:
myRawConns[bufConn] = true
go handleRaw(bufConn)
case bufConn := <- delTcpChat:
bufConn.Close();
// we can safely ignore this error
bufConn.Close()
delete(myRawConns, bufConn)
case bufConn := <- newHttpChat:
go handleSorted(bufConn)
//case msg := <- myChans["general"]:
//delete(myChans["general"], bufConn)
//case msg := <- myRooms["general"]:
//delete(myRooms["general"], bufConn)
case msg := <- myMsgs:
ts, err := msg.receivedAt.MarshalJSON()
if nil != err {
fmt.Fprintf(os.Stderr, "[Error] %s\n", err)
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"
}
fmt.Fprintf(os.Stdout, "[Timestamp] %s\n", ts)
fmt.Fprintf(os.Stdout, "[Remote] %s\n", msg.sender.RemoteAddr().String())
fmt.Fprintf(os.Stdout, "[Message] %s\n", msg.bytes);
// 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
}
// backlogged connections could prevent a next write,
// so this should be refactored into a goroutine
// And what to do about slow clients that get behind (or DoS)?
// SetDeadTime and Disconnect them?
conn.Write(msg.bytes)
// 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")