An example chat server in golang.
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 

215 lignes
6.6 KiB

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) {
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
}
}
}