217 lignes
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			217 lignes
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package main
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"os"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| type telnetUser struct {
 | |
| 	bufConn   bufferedConn
 | |
| 	userCount chan int
 | |
| 	email     string
 | |
| 	newMsg    chan string
 | |
| }
 | |
| 
 | |
| // Trying to keep it slim with just one goroutine per client for each reads and writes.
 | |
| // Initially I was spawning a goroutine per write in the main select, but my guess is that
 | |
| // constantly allocating and cleaning up 4k of memory (or perhaps less these days
 | |
| // https://blog.nindalf.com/posts/how-goroutines-work/) is probably not very efficient for
 | |
| // small tweet-sized network writes. Also, I like this style better
 | |
| // TODO: Learn if it matters at all to have fewer long-lived vs more short-lived goroutines
 | |
| 
 | |
| // Auth & Reads
 | |
| func handleTelnetConn(bufConn bufferedConn) {
 | |
| 	// Used as a reference: https://jameshfisher.com/2017/04/18/golang-tcp-server.html
 | |
| 
 | |
| 	var email string
 | |
| 	var code string
 | |
| 	var authn bool
 | |
| 
 | |
| 	// Handle all subsequent packets
 | |
| 	buffer := make([]byte, 1024)
 | |
| 	var u *telnetUser
 | |
| 	for {
 | |
| 		//fmt.Fprintf(os.Stdout, "[raw] Waiting for message...\n")
 | |
| 		count, err := bufConn.Read(buffer)
 | |
| 		if nil != err {
 | |
| 			if io.EOF != err {
 | |
| 				fmt.Fprintf(os.Stderr, "Non-EOF socket error: %s\n", err)
 | |
| 			} else {
 | |
| 				broadcastMsg <- chatMsg{
 | |
| 					sender:     nil,
 | |
| 					Message:    fmt.Sprintf("<%s> left #general\r\n", u.email),
 | |
| 					ReceivedAt: time.Now(),
 | |
| 					Channel:    "general",
 | |
| 					User:       "system",
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			if nil != u {
 | |
| 				cleanTelnet <- *u
 | |
| 			}
 | |
| 			break
 | |
| 		}
 | |
| 		msg := string(buffer[:count])
 | |
| 		if "" == strings.TrimSpace(msg) {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// Rate Limit: Reasonable poor man's DoS prevention (Part 1)
 | |
| 		// A human does not send messages super fast and blocking the
 | |
| 		// writes of other incoming messages to this client for this long
 | |
| 		// won't hinder the user experience (and may in fact enhance it)
 | |
| 		// TODO: should do this for HTTP as well (or, better yet, implement hashcash)
 | |
| 		time.Sleep(150 * time.Millisecond)
 | |
| 
 | |
| 		// 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, "[SANITY FAIL] using a 0-length buffer")
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		if !authn {
 | |
| 			if "" == email {
 | |
| 				// 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(msg)
 | |
| 				emailParts := strings.Split(email, "@")
 | |
| 				if 2 != len(emailParts) {
 | |
| 					email = ""
 | |
| 					fmt.Fprintf(bufConn, "Email: ")
 | |
| 					continue
 | |
| 				}
 | |
| 
 | |
| 				// 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
 | |
| 				wg := sync.WaitGroup{}
 | |
| 				wg.Add(1)
 | |
| 				go func() {
 | |
| 					time.Sleep(50 * time.Millisecond)
 | |
| 					const msg = "Mailing auth code..."
 | |
| 					for _, r := range msg {
 | |
| 						time.Sleep(20 * time.Millisecond)
 | |
| 						fmt.Fprintf(bufConn, string(r))
 | |
| 					}
 | |
| 					time.Sleep(50 * time.Millisecond)
 | |
| 					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== TELNET AUTHORIZATION ==\n[cheat code for %s]: %s\n", email, code)
 | |
| 				time.Sleep(150 * time.Millisecond)
 | |
| 				fmt.Fprintf(bufConn, " done\n")
 | |
| 				time.Sleep(150 * time.Millisecond)
 | |
| 				fmt.Fprintf(bufConn, "Auth Code: ")
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			if code != strings.TrimSpace(msg) {
 | |
| 				fmt.Fprintf(bufConn, "Incorrect Code\nAuth Code: ")
 | |
| 			} else {
 | |
| 				authn = true
 | |
| 				time.Sleep(150 * time.Millisecond)
 | |
| 				fmt.Fprintf(bufConn, "\n")
 | |
| 				u = &telnetUser{
 | |
| 					bufConn:   bufConn,
 | |
| 					email:     email,
 | |
| 					userCount: make(chan int, 1),
 | |
| 					newMsg:    make(chan string, 10), // reasonably sized
 | |
| 				}
 | |
| 				authTelnet <- *u
 | |
| 				// prevent data race on len(telnetConns)
 | |
| 				count := <-u.userCount
 | |
| 				close(u.userCount)
 | |
| 				u.userCount = nil
 | |
| 
 | |
| 				// Note: There's a 500ms gap between when we accept the client
 | |
| 				// and when it can start receiving messages and when it begins
 | |
| 				// to handle them, however, it's unlikely that >= 10 messages
 | |
| 				// will simultaneously flood in during that time
 | |
| 
 | |
| 				time.Sleep(50 * time.Millisecond)
 | |
| 				fmt.Fprintf(bufConn, "\n")
 | |
| 				time.Sleep(50 * time.Millisecond)
 | |
| 				// It turns out that ANSI characters work in Telnet just fine
 | |
| 				fmt.Fprintf(bufConn, "\033[1;32m"+"Welcome to #general (%d users)!"+"\033[22;39m", count)
 | |
| 				time.Sleep(50 * time.Millisecond)
 | |
| 				fmt.Fprintf(bufConn, "\n")
 | |
| 				time.Sleep(50 * time.Millisecond)
 | |
| 				// TODO /help /join <room> /users /channels /block <user> /upgrade <http/ws>
 | |
| 				//fmt.Fprintf(bufConn, "(TODO `/help' for list of commands)")
 | |
| 				time.Sleep(100 * time.Millisecond)
 | |
| 				fmt.Fprintf(bufConn, "\n")
 | |
| 
 | |
| 				// Would be cool to write a prompt...
 | |
| 				// I wonder if I could send fudge some ANSI codes to keep the prompt
 | |
| 				// even when new messages come in, but not overwrite what he user typed...
 | |
| 				//fmt.Fprintf(bufConn, "\n%s> ", email)
 | |
| 
 | |
| 				go handleTelnetBroadcast(u)
 | |
| 			}
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		broadcastMsg <- chatMsg{
 | |
| 			ReceivedAt: time.Now(),
 | |
| 			sender:     bufConn,
 | |
| 			Message:    strings.TrimRight(msg, "\r\n"),
 | |
| 			Channel:    "general",
 | |
| 			User:       email,
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Writes (post Auth)
 | |
| func handleTelnetBroadcast(u *telnetUser) {
 | |
| 	for {
 | |
| 		msg, more := <-u.newMsg
 | |
| 		if !more {
 | |
| 			// channel was closed
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		// Disallow Reverse Rate Limit: Reasonable poor man's DoS prevention (Part 3)
 | |
| 		// https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/
 | |
| 		timeoutDuration := 2 * time.Second
 | |
| 		u.bufConn.SetWriteDeadline(time.Now().Add(timeoutDuration))
 | |
| 		_, err := fmt.Fprintf(u.bufConn, msg+"\r\n")
 | |
| 		if nil != err {
 | |
| 			cleanTelnet <- *u
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| }
 |