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