An example chat server in golang.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

559 lines
16 KiB

package main
// TODO learn about chan chan's
// http://marcio.io/2015/07/handling-1-million-requests-per-minute-with-golang/
import (
"bufio"
6 years ago
"crypto/rand"
"encoding/base64"
"flag"
"fmt"
"io"
"io/ioutil"
"net"
6 years ago
"net/http"
"net/url"
"os"
"strconv"
6 years ago
"strings"
"sync"
"time"
"gopkg.in/yaml.v2"
)
// I'm not sure how to pass nested structs, so I de-nested this.
// TODO: Learn if passing nested structs is desirable?
type Conf struct {
Port uint `yaml:"port,omitempty"`
Mailer ConfMailer
}
6 years ago
type ConfMailer struct {
Url string `yaml:"url,omitempty"`
ApiKey string `yaml:"api_key,omitempty"`
From string `yaml:"from,omitempty"`
}
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 {
r *bufio.Reader
rout io.Reader
net.Conn
}
func newBufferedConn(c net.Conn) bufferedConn {
return bufferedConn{bufio.NewReader(c), nil, c}
}
func (b bufferedConn) Peek(n int) ([]byte, error) {
return b.r.Peek(n)
}
func (b bufferedConn) Buffered() (int) {
return b.r.Buffered()
}
func (b bufferedConn) Read(p []byte) (int, error) {
if b.rout != nil {
return b.rout.Read(p)
}
return b.r.Read(p)
}
// Just making these all globals right now
// because... I can clean it up later
type myMsg struct {
sender net.Conn
bytes []byte
receivedAt time.Time
6 years ago
channel string
email string
}
var firstMsgs chan myMsg
//var myRooms map[string](chan myMsg)
var myMsgs chan myMsg
//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
func usage() {
fmt.Fprintf(os.Stderr, "\nusage: go run chatserver.go\n")
flag.PrintDefaults();
fmt.Println()
os.Exit(1)
}
6 years ago
// https://blog.questionable.services/article/generating-secure-random-numbers-crypto-rand/
func genAuthCode() (string, error) {
n := 12
b := make([]byte, n)
_, err := rand.Read(b)
// Note that err == nil only if we read len(b) bytes.
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
func handleRaw(bufConn bufferedConn) {
// TODO
// What happens if this is being read from range
// when it's being added here (data race)?
// Should I use a channel here instead?
// TODO see https://jameshfisher.com/2017/04/18/golang-tcp-server.html
6 years ago
var email string
var code string
var authn bool
// Handle all subsequent packets
6 years ago
buffer := make([]byte, 1024)
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)
}
fmt.Fprintf(os.Stdout, "Ending socket\n")
6 years ago
delTcpChat <- bufConn
break
}
6 years ago
buf := buffer[:count]
// 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, "Weird")
6 years ago
break
}
if !authn {
if "" == email {
// Indeed telnet sends CRLF as part of the message
//fmt.Fprintf(os.Stdout, "buf{%s}\n", buf[:count])
6 years ago
// TODO use safer email testing
email = strings.TrimSpace(string(buf[:count]))
emailParts := strings.Split(email, "@")
if 2 != len(emailParts) {
fmt.Fprintf(bufConn, "Email: ")
6 years ago
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
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()
6 years ago
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)
6 years ago
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: ")
6 years ago
continue
}
if code != strings.TrimSpace(string(buf[:count])) {
fmt.Fprintf(bufConn, "Incorrect Code\nAuth Code: ")
6 years ago
} else {
authn = true
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)
6 years ago
}
continue
}
6 years ago
//fmt.Fprintf(os.Stdout, "Queing message...\n");
//myRooms["general"] <- myMsg{
myMsgs <- myMsg{
receivedAt: time.Now(),
sender: bufConn,
bytes: buf[0:count],
6 years ago
channel: "general",
email: email,
}
//fmt.Fprintf(bufConn, "> ")
}
}
func handleSorted(conn bufferedConn) {
// 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 {
panic(err)
}
firstMsgs <- myMsg{
receivedAt: time.Now(),
sender: conn,
bytes: firstMsg,
6 years ago
channel: "general",
}
// TODO
// * TCP-CHAT
// * HTTP
// * TLS
// Handle all subsequent packets
buf := make([]byte, 1024)
for {
fmt.Fprintf(os.Stdout, "[sortable] Waiting for message...\n");
count, err := conn.Read(buf)
if nil != err {
if io.EOF != err {
fmt.Fprintf(os.Stderr, "Non-EOF socket error: %s\n", err)
}
fmt.Fprintf(os.Stdout, "Ending socket\n")
break
}
// 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, "Weird")
continue
}
//myRooms["general"] <- myMsg{
myMsgs <- myMsg{
receivedAt: time.Now(),
sender: conn,
bytes: buf[0:count],
6 years ago
channel: "general",
}
}
}
6 years ago
func handleConnection(netConn net.Conn) {
ts := time.Now()
fmt.Fprintf(os.Stdout, "[New Connection] (%s) welcome %s\n", ts, netConn.RemoteAddr().String())
m := sync.Mutex{}
virgin := true
// Why don't these work?
//buf := make([]byte, 0, 1024)
//buf := []byte{}
// But this does
6 years ago
bufConn := newBufferedConn(netConn)
//myUnsortedConns[bufConn] = true
go func() {
// Handle First Packet
_, err := bufConn.Peek(1)
//fmsg, err := bufConn.Peek(1)
if nil != err {
panic(err)
}
//fmt.Fprintf(os.Stdout, "[First Byte] %s\n", fmsg)
m.Lock();
if virgin {
virgin = false
newHttpChat <- bufConn
} else {
6 years ago
// TODO probably needs to go into a channel
newTcpChat <- bufConn
}
m.Unlock();
}()
time.Sleep(250 * 1000000)
// If we still haven't received data from the client
// assume that the client must be expecting a welcome from us
m.Lock()
if virgin {
virgin = false
// don't block for this
// let it be handled after the unlock
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()
}
6 years ago
func sendAuthCode(cnf ConfMailer, to string) (string, error) {
code, err := genAuthCode()
if nil != err {
return "", err
}
// TODO use go text templates with HTML escaping
text := "Your authorization code:\n\n" + code
html := "Your authorization code:<br><br>" + code
// https://stackoverflow.com/questions/24493116/how-to-send-a-post-request-in-go
// https://stackoverflow.com/questions/16673766/basic-http-auth-in-go
client := http.Client{}
form := url.Values{}
form.Add("from", cnf.From)
form.Add("to", to)
form.Add("subject", "Sample Chat Auth Code: " + code)
form.Add("text", text)
form.Add("html", html)
req, err := http.NewRequest("POST", cnf.Url, strings.NewReader(form.Encode()))
if nil != err {
return "", err
}
//req.PostForm = form
req.Header.Add("User-Agent", "golang http.Client - Sample Chat App Authenticator")
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.SetBasicAuth("api", cnf.ApiKey)
resp, err := client.Do(req)
if nil != err {
return "", err
}
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
6 years ago
body, err := ioutil.ReadAll(resp.Body)
if nil != err {
return "", err
}
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)
}
6 years ago
return code, nil
}
var config Conf
func main() {
flag.Usage = usage
port := flag.Uint("telnet-port", 0, "tcp telnet chat port")
6 years ago
confname := flag.String("conf", "./config.yml", "yaml config file")
flag.Parse()
confstr, err := ioutil.ReadFile(*confname)
6 years ago
fmt.Fprintf(os.Stdout, "-conf=%s\n", *confname)
if nil != err {
fmt.Fprintf(os.Stderr, "%s\nUsing defaults instead\n", err)
confstr = []byte("{\"port\":" + strconv.Itoa(int(*port)) + "}")
}
err = yaml.Unmarshal(confstr, &config)
if nil != err {
config = Conf{}
}
myRawConns := make(map[bufferedConn]bool)
firstMsgs = make(chan myMsg, 128)
//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)
//myUnsortedConns = make(map[net.Conn]bool)
6 years ago
// TODO dynamically select on channels?
// https://stackoverflow.com/questions/19992334/how-to-listen-to-n-channels-dynamic-select-statement
//myRooms["general"] = make(chan myMsg, 128)
myMsgs = make(chan myMsg, 128)
6 years ago
var addr string
if 0 != int(*port) {
addr = ":" + strconv.Itoa(int(*port))
} else {
addr = ":" + strconv.Itoa(int(config.Port))
}
// https://golang.org/pkg/net/#Conn
sock, err := net.Listen("tcp", addr)
if nil != err {
fmt.Fprintf(os.Stderr, "Couldn't bind to TCP socket %q: %s\n", addr, err)
os.Exit(2)
}
fmt.Println("Listening on", addr);
go func() {
for {
conn, err := sock.Accept()
if err != nil {
// Not sure what kind of error this could be or how it could happen.
// Could a connection abort or end before it's handled?
fmt.Fprintf(os.Stderr, "Error accepting connection:\n%s\n", err)
}
newConns <- conn
}
}()
// Main event loop handling most access to shared data
for {
select {
case conn := <- newConns:
// 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:
go handleRaw(bufConn)
case bufConn := <- delTcpChat:
// we can safely ignore this error
bufConn.Close()
delete(myRawConns, bufConn)
case bufConn := <- newHttpChat:
go handleSorted(bufConn)
//case msg := <- myRooms["general"]:
//delete(myRooms["general"], bufConn)
case msg := <- myMsgs:
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"
}
// 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
}
// 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")
ts, err := msg.receivedAt.MarshalJSON()
if nil != err {
fmt.Fprintf(os.Stderr, "f [Error] %s\n", err)
}
fmt.Fprintf(os.Stdout, "f [Timestamp] %s\n", ts)
fmt.Fprintf(os.Stdout, "f [Remote] %s\n", msg.sender.RemoteAddr().String())
fmt.Fprintf(os.Stdout, "f [Message] %s\n", msg.bytes);
}
}
}