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.

559 lignes
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"
il y a 6 ans
"crypto/rand"
"encoding/base64"
"flag"
"fmt"
"io"
"io/ioutil"
"net"
il y a 6 ans
"net/http"
"net/url"
"os"
"strconv"
il y a 6 ans
"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
}
il y a 6 ans
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
il y a 6 ans
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)
}
il y a 6 ans
// 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
il y a 6 ans
var email string
var code string
var authn bool
// Handle all subsequent packets
il y a 6 ans
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")
il y a 6 ans
delTcpChat <- bufConn
break
}
il y a 6 ans
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")
il y a 6 ans
break
}
if !authn {
if "" == email {
// Indeed telnet sends CRLF as part of the message
//fmt.Fprintf(os.Stdout, "buf{%s}\n", buf[:count])
il y a 6 ans
// TODO use safer email testing
email = strings.TrimSpace(string(buf[:count]))
emailParts := strings.Split(email, "@")
if 2 != len(emailParts) {
fmt.Fprintf(bufConn, "Email: ")
il y a 6 ans
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()
il y a 6 ans
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)
il y a 6 ans
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: ")
il y a 6 ans
continue
}
if code != strings.TrimSpace(string(buf[:count])) {
fmt.Fprintf(bufConn, "Incorrect Code\nAuth Code: ")
il y a 6 ans
} 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)
il y a 6 ans
}
continue
}
il y a 6 ans
//fmt.Fprintf(os.Stdout, "Queing message...\n");
//myRooms["general"] <- myMsg{
myMsgs <- myMsg{
receivedAt: time.Now(),
sender: bufConn,
bytes: buf[0:count],
il y a 6 ans
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,
il y a 6 ans
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],
il y a 6 ans
channel: "general",
}
}
}
il y a 6 ans
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
il y a 6 ans
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 {
il y a 6 ans
// 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()
}
il y a 6 ans
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
il y a 6 ans
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)
}
il y a 6 ans
return code, nil
}
var config Conf
func main() {
flag.Usage = usage
port := flag.Uint("telnet-port", 0, "tcp telnet chat port")
il y a 6 ans
confname := flag.String("conf", "./config.yml", "yaml config file")
flag.Parse()
confstr, err := ioutil.ReadFile(*confname)
il y a 6 ans
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)
il y a 6 ans
// 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)
il y a 6 ans
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);
}
}
}