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/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 {
Addr string ` yaml:"addr,omitempty" `
Port uint ` yaml:"port,omitempty" `
Mailer ConfMailer
RootPath string ` yaml:"root_path,omitempty" `
}
type ConfMailer struct {
Url string ` yaml:"url,omitempty" `
ApiKey string ` yaml:"api_key,omitempty" `
From string ` yaml:"from,omitempty" `
}
// 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 ) , 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 )
}
type chatMsg struct {
sender net . Conn
Message string ` json:"message" `
ReceivedAt time . Time ` json:"received_at" `
Channel string ` json:"channel" `
User string ` json:"user" `
}
// Poor-Man's container/ring (circular buffer)
type chatHist struct {
msgs [ ] * chatMsg
i int // current index
c int // current count (number of elements)
}
// Multi-use
var config Conf
var virginConns chan net . Conn
var gotClientHello chan bufferedConn
var myChatHist chatHist
var broadcastMsg chan chatMsg
// Telnet
var wantsServerHello chan bufferedConn
var authTelnet chan telnetUser
var cleanTelnet chan telnetUser
// HTTP
var demuxHttpClient chan bufferedConn
var authReqs 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 )
}
// 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 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 {
// Throw away the first bytes
b := make ( [ ] byte , 4096 )
conn . Read ( b )
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: " )
wantsServerHello <- conn
return
} else if "HTTP" != protocol {
defer conn . Close ( )
fmt . Fprintf ( conn , "\n\nNot yet supported. Try HTTP or Telnet\n\n" )
return
}
demuxHttpClient <- conn
}
func testForHello ( 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
bufConn := newBufferedConn ( netConn )
go func ( ) {
// Cause first packet to be loaded into buffer
_ , err := bufConn . Peek ( 1 )
if nil != err {
panic ( err )
}
m . Lock ( )
if virgin {
virgin = false
gotClientHello <- bufConn
} else {
wantsServerHello <- bufConn
}
m . Unlock ( )
} ( )
// Wait for a hello packet of some sort from the client
// (obviously this wouldn't work in extremely high latency situations)
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
// Defer as to not block and prolonging the mutex
// (not that those few cycles much matter...)
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 ( )
}
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
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
}
func main ( ) {
flag . Usage = usage
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 Maybe embed the public dir into the binary
// (and provide a flag with path for override - like gitea)
config . RootPath = "./public"
}
// The magical sorting hat
virginConns = make ( chan net . Conn , 128 )
// TCP & Authentication
telnetConns := make ( map [ bufferedConn ] telnetUser )
wantsServerHello = make ( chan bufferedConn , 128 )
authTelnet = make ( chan telnetUser , 128 )
// HTTP & Authentication
myAuthReqs := make ( map [ string ] authReq )
authReqs = make ( chan authReq , 128 )
valAuthReqs = make ( chan authReq , 128 )
delAuthReqs = make ( chan authReq , 128 )
gotClientHello = make ( chan bufferedConn , 128 )
demuxHttpClient = make ( chan bufferedConn , 128 )
//myRooms["general"] = make(chan chatMsg, 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 chatMsg , 128 )
myChatHist . msgs = make ( [ ] * chatMsg , 128 )
var addr string
if 0 != int ( * port ) {
addr = config . Addr + ":" + strconv . Itoa ( int ( * port ) )
} else {
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 ) )
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 ,
}
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 testForHello ( conn )
case u := <- authTelnet :
// allow to receive messages
// (and be counted among the users)
telnetConns [ u . bufConn ] = u
// is chan chan the right way to handle this?
u . userCount <- len ( telnetConns )
broadcastMsg <- chatMsg {
sender : nil ,
Message : fmt . Sprintf ( "<%s> joined #general\r\n" , u . email ) ,
ReceivedAt : time . Now ( ) ,
Channel : "general" ,
User : "system" ,
}
case ar := <- authReqs :
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 {
// 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 )
case bufConn := <- wantsServerHello :
go handleTelnetConn ( bufConn )
case u := <- cleanTelnet :
close ( u . newMsg )
// we can safely ignore this error, if any
u . bufConn . Close ( )
delete ( telnetConns , u . bufConn )
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
// since we manage the session by HTTP Auth Bearer rather than TCP
myHttpServer . chans <- bufConn
case msg := <- broadcastMsg :
// copy comes in, pointer gets saved (and not GC'd, I hope)
myChatHist . msgs [ myChatHist . i ] = & msg
myChatHist . i += 1
if myChatHist . c < cap ( myChatHist . msgs ) {
myChatHist . c += 1
}
myChatHist . i %= len ( myChatHist . msgs )
// 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 a Telnet client's tz
// ... could probably make time for this in the authentication loop
zone , _ := msg . ReceivedAt . Zone ( )
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 )
for _ , u := range telnetConns {
// Don't echo back to the original client
if msg . sender == u . bufConn {
continue
}
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.
cleanTelnet <- u
}
}
}
}
}