mostly just cleanup I think...
This commit is contained in:
		
							parent
							
								
									34ee3924d3
								
							
						
					
					
						commit
						64425b41d2
					
				
							
								
								
									
										216
									
								
								chatserver.go
									
									
									
									
									
								
							
							
						
						
									
										216
									
								
								chatserver.go
									
									
									
									
									
								
							| @ -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)) | ||||
| 
 | ||||
|         // 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") | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user