package main
// TODO learn about chan chan's
// http://marcio.io/2015/07/handling-1-million-requests-per-minute-with-golang/
import (
"bufio"
"crypto/rand"
"encoding/base64"
"flag"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"strconv"
"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
}
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
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 )
}
// 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
var email string
var code string
var authn bool
// Handle all subsequent packets
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" )
delTcpChat <- bufConn
break
}
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" )
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 ( string ( buf [ : count ] ) )
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
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 ( )
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== 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: " )
continue
}
if code != strings . TrimSpace ( string ( buf [ : count ] ) ) {
fmt . Fprintf ( bufConn , "Incorrect Code\nAuth Code: " )
} 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)
}
continue
}
//fmt.Fprintf(os.Stdout, "Queing message...\n");
//myRooms["general"] <- myMsg{
myMsgs <- myMsg {
receivedAt : time . Now ( ) ,
sender : bufConn ,
bytes : buf [ 0 : count ] ,
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 ,
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 ] ,
channel : "general" ,
}
}
}
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
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 {
// 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 ( )
}
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 )
} else {
fmt . Fprintf ( os . Stdout , "[Mailgun] Status: %d" , resp . StatusCode )
}
return code , nil
}
var config Conf
func main ( ) {
flag . Usage = usage
port := flag . Uint ( "telnet-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 { }
}
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)
// 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 )
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 ) ;
}
}
}