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.

521 lignes
15 KiB

package main
// TODO learn more about chan chan's
// http://marcio.io/2015/07/handling-1-million-requests-per-minute-with-golang/
import (
"bufio"
"bytes"
"crypto/rand"
"encoding/base64"
"flag"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/emicklei/go-restful"
"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 {
il y a 6 ans
Addr string `yaml:"addr,omitempty"`
Port uint `yaml:"port,omitempty"`
Mailer ConfMailer
RootPath string `yaml:"root_path,omitempty"`
}
il y a 6 ans
type ConfMailer struct {
Url string `yaml:"url,omitempty"`
ApiKey string `yaml:"api_key,omitempty"`
From string `yaml:"from,omitempty"`
il y a 6 ans
}
type tcpUser struct {
bufConn bufferedConn
userCount chan int
email string
il y a 6 ans
newMsg chan 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 // See https://github.com/polvi/sni/blob/master/sni.go#L135
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
Message string `json:"message"`
ReceivedAt time.Time `json:"received_at"`
Channel string `json:"channel"`
User string `json:"user"`
}
type JsonMsg struct {
il y a 6 ans
Messages []*myMsg `json:"messages"`
}
il y a 6 ans
type chatHist struct {
msgs []*myMsg
i int
c int
}
var myChatHist chatHist
var broadcastMsg chan myMsg
var virginConns chan net.Conn
il y a 6 ans
var wantsServerHello chan bufferedConn
il y a 6 ans
var authTelnet chan tcpUser
var cleanTelnet chan tcpUser
il y a 6 ans
var gotClientHello chan bufferedConn
il y a 6 ans
// HTTP
il y a 6 ans
var demuxHttpClient chan bufferedConn
var newAuthReqs chan authReq
var valAuthReqs chan authReq
var delAuthReqs chan authReq
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
il y a 6 ans
b := make([]byte, n)
_, err := rand.Read(b)
// Note that err == nil only if we read len(b) bytes.
il y a 6 ans
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
il y a 6 ans
func muxTcp(conn bufferedConn) {
// Wish List for protocol detection
// * PROXY protocol (and loop)
// * HTTP CONNECT (proxy) (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
// However, typical MTU is 1,500 and HTTP can have a 2k URL
// so expecting to find the "HTTP/1.1" in the Peek is not always reasonable
n := conn.Buffered()
firstMsg, err := conn.Peek(n)
if nil != err {
conn.Close()
return
}
var protocol string
// between A and z
if firstMsg[0] >= 65 && firstMsg[0] <= 122 {
i := bytes.Index(firstMsg, []byte(" /"))
if -1 != i {
protocol = "HTTP"
// very likely HTTP
j := bytes.IndexAny(firstMsg, "\r\n")
if -1 != j {
k := bytes.Index(bytes.ToLower(firstMsg[:j]), []byte("HTTP/1"))
if -1 != k {
// positively HTTP
}
}
}
} else if 0x16 /*22*/ == firstMsg[0] {
// Because I don't always remember off the top of my head what the first byte is
// http://blog.fourthbit.com/2014/12/23/traffic-analysis-of-an-ssl-slash-tls-session
// https://tlseminar.github.io/first-few-milliseconds/
// TODO I want to learn about ALPN
protocol = "TLS"
}
if "" == protocol {
fmt.Fprintf(conn, "\n\nWelcome to Sample Chat! You're not an HTTP client, assuming Telnet.\nYou must authenticate via email to participate\n\nEmail: ")
il y a 6 ans
wantsServerHello <- conn
return
} else if "HTTP" != protocol {
defer conn.Close()
fmt.Fprintf(conn, "\n\nNot yet supported. Try HTTP or Telnet\n\n")
return
}
il y a 6 ans
demuxHttpClient <- conn
}
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
il y a 6 ans
bufConn := newBufferedConn(netConn)
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
il y a 6 ans
gotClientHello <- bufConn
} else {
il y a 6 ans
wantsServerHello <- 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 (http is also available on this port)."+
"\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
}
il y a 6 ans
// TODO use go text templates with HTML escaping
text := "Your authorization code:\n\n" + code
html := "Your authorization code:<br><br>" + code
il y a 6 ans
// https://stackoverflow.com/questions/24493116/how-to-send-a-post-request-in-go
// https://stackoverflow.com/questions/16673766/basic-http-auth-in-go
il y a 6 ans
client := http.Client{}
form := url.Values{}
form.Add("from", cnf.From)
form.Add("to", to)
form.Add("subject", "Sample Chat Auth Code: "+code)
il y a 6 ans
form.Add("text", text)
form.Add("html", html)
req, err := http.NewRequest("POST", cnf.Url, strings.NewReader(form.Encode()))
if nil != err {
return "", err
}
il y a 6 ans
//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
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)
}
return code, nil
il y a 6 ans
}
var config Conf
func main() {
flag.Usage = usage
il y a 6 ans
port := flag.Uint("port", 0, "tcp telnet chat port")
confname := flag.String("conf", "./config.yml", "yaml config file")
flag.Parse()
confstr, err := ioutil.ReadFile(*confname)
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{}
}
if "" == config.RootPath {
// TODO Embed the public dir at the default
config.RootPath = "./public"
}
// The magical sorting hat
virginConns = make(chan net.Conn, 128)
// TCP & Authentication
il y a 6 ans
telnetConns := make(map[bufferedConn]tcpUser)
il y a 6 ans
wantsServerHello = make(chan bufferedConn, 128)
il y a 6 ans
authTelnet = make(chan tcpUser, 128)
// HTTP & Authentication
myAuthReqs := make(map[string]authReq)
newAuthReqs = make(chan authReq, 128)
valAuthReqs = make(chan authReq, 128)
delAuthReqs = make(chan authReq, 128)
il y a 6 ans
gotClientHello = make(chan bufferedConn, 128)
demuxHttpClient = make(chan bufferedConn, 128)
// cruft to delete
//myRooms = make(map[string](chan myMsg))
//myRooms["general"] = make(chan myMsg, 128)
// Note: I had considered dynamically select on channels for rooms.
// https://stackoverflow.com/questions/19992334/how-to-listen-to-n-channels-dynamic-select-statement
// I don't think that's actually the best approach, but I just wanted to save the link
broadcastMsg = make(chan myMsg, 128)
// Poor-Man's container/ring (circular buffer)
il y a 6 ans
myChatHist.msgs = make([]*myMsg, 128)
var addr string
if 0 != int(*port) {
il y a 6 ans
addr = config.Addr + ":" + strconv.Itoa(int(*port))
} else {
il y a 6 ans
addr = config.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)
}
virginConns <- conn
}
}()
// Learning by Example
// https://github.com/emicklei/go-restful/blob/master/examples/restful-multi-containers.go
// https://github.com/emicklei/go-restful/blob/master/examples/restful-basic-authentication.go
// https://github.com/emicklei/go-restful/blob/master/examples/restful-serve-static.go
// https://github.com/emicklei/go-restful/blob/master/examples/restful-pre-post-filters.go
container := restful.NewContainer()
wsStatic := new(restful.WebService)
wsStatic.Path("/")
wsStatic.Route(wsStatic.GET("/").To(serveStatic))
wsStatic.Route(wsStatic.GET("/{subpath:*}").To(serveStatic))
container.Add(wsStatic)
cors := restful.CrossOriginResourceSharing{ExposeHeaders: []string{"Authorization"}, CookiesAllowed: false, Container: container}
wsApi := new(restful.WebService)
wsApi.Path("/api").Consumes(restful.MIME_JSON).Produces(restful.MIME_JSON).Filter(cors.Filter)
wsApi.Route(wsApi.GET("/hello").To(serveHello))
wsApi.Route(wsApi.POST("/sessions").To(requestAuth))
wsApi.Route(wsApi.POST("/sessions/{cid}").To(issueToken))
il y a 6 ans
wsApi.Route(wsApi.GET("/rooms/{room}").Filter(requireToken).To(listMsgs))
wsApi.Route(wsApi.POST("/rooms/{room}").Filter(requireToken).To(postMsg))
container.Add(wsApi)
server := &http.Server{
Addr: addr,
Handler: container,
}
il y a 6 ans
myHttpServer := newHttpServer(sock)
go func() {
server.Serve(myHttpServer)
}()
// Main event loop handling most access to shared data
for {
select {
case conn := <-virginConns:
// This is short lived
go handleConnection(conn)
il y a 6 ans
case u := <-authTelnet:
// allow to receive messages
// (and be counted among the users)
il y a 6 ans
telnetConns[u.bufConn] = u
// is chan chan the right way to handle this?
il y a 6 ans
u.userCount <- len(telnetConns)
broadcastMsg <- myMsg{
sender: nil,
// TODO fmt.Fprintf()? template?
Message: "<" + u.email + "> joined #general\n",
ReceivedAt: time.Now(),
Channel: "general",
User: "system",
}
case ar := <-newAuthReqs:
myAuthReqs[ar.Cid] = ar
case ar := <-valAuthReqs:
// TODO In this case it's probably more conventional (and efficient) to
// use struct with a mutex and the authReqs map than a chan chan
av, ok := myAuthReqs[ar.Cid]
//ar.Chan <- nil // TODO
if ok {
ar.Chan <- av
} else {
il y a 6 ans
// sending empty object so that I can still send a copy
// rather than a pointer above. Maybe not the right way
// to do this, but it works for now.
ar.Chan <- authReq{}
}
case ar := <-delAuthReqs:
delete(myAuthReqs, ar.Cid)
il y a 6 ans
case bufConn := <-wantsServerHello:
go handleTelnetConn(bufConn)
il y a 6 ans
case u := <-cleanTelnet:
il y a 6 ans
// we can safely ignore this error, if any
il y a 6 ans
if "" != u.email {
broadcastMsg <- myMsg{
sender: nil,
// TODO fmt.Fprintf()? template?
Message: "<" + u.email + "> left #general\n",
ReceivedAt: time.Now(),
Channel: "general",
User: "system",
}
}
close(u.newMsg)
il y a 6 ans
u.bufConn.Close()
il y a 6 ans
delete(telnetConns, u.bufConn)
il y a 6 ans
case bufConn := <-gotClientHello:
go muxTcp(bufConn)
case bufConn := <-demuxHttpClient:
// this will be Accept()ed immediately by the go-restful container
// NOTE: we don't store these HTTP connections for broadcast
il y a 6 ans
// since we manage the session by HTTP Auth Bearer rather than TCP
myHttpServer.chans <- bufConn
case msg := <-broadcastMsg:
il y a 6 ans
// copy comes in, pointer gets saved (and not GC'd, I hope)
myChatHist.msgs[myChatHist.i] = &msg
il y a 6 ans
myChatHist.i += 1
if myChatHist.c < cap(myChatHist.msgs) {
myChatHist.c += 1
}
myChatHist.i %= len(myChatHist.msgs)
il y a 6 ans
// print the system message (the "log")
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"
}
// Tangential thought:
// 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()
il y a 6 ans
fmt.Fprintf(os.Stdout, tf+" [%s] (%s): %s\r\n",
t.Year(), t.Month(), t.Day(),
t.Hour(), t.Minute(), t.Second(), zone,
sender,
msg.User, msg.Message)
il y a 6 ans
for _, u := range telnetConns {
// Don't echo back to the original client
il y a 6 ans
if msg.sender == u.bufConn {
continue
}
il y a 6 ans
msg := fmt.Sprintf(tf+" [%s]: %s", t.Year(), t.Month(), t.Day(), t.Hour(),
t.Minute(), t.Second(), zone, msg.User, msg.Message)
select {
case u.newMsg <- msg:
// all is well, client was ready to receive
default:
// Rate Limit: Reasonable poor man's DoS prevention (Part 2)
// This client's send channel buffer is full.
// It is consuming data too slowly. It may be malicious.
// In the case that it's experiencing network issues,
// well, these things happen when you're having network issues.
// It can reconnect.
il y a 6 ans
cleanTelnet <- u
il y a 6 ans
}
/*
// To ask: Why do I have to pass in conn to prevent a data race? Is it garbage collection?
// Don't block the rest of the loop
// TODONE maybe use a chan to send to the socket's event loop
// (left this in to remind myself to ask questions)
go func(conn bufferedConn) {
// Protect against malicious clients to prevent DoS
// https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/
timeoutDuration := 2 * time.Second
conn.SetWriteDeadline(time.Now().Add(timeoutDuration))
_, err := fmt.Fprintf(conn, msg)
if nil != err {
il y a 6 ans
cleanTelnet <- u
il y a 6 ans
}
}(conn)
*/
}
}
}
}