916 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			916 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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"
 | |
| 	"crypto/subtle"
 | |
| 	"encoding/base64"
 | |
| 	"flag"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"io/ioutil"
 | |
| 	"net"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"os"
 | |
| 	"path"
 | |
| 	"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 {
 | |
| 	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"`
 | |
| }
 | |
| 
 | |
| 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 // 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 {
 | |
| 	Messages []myMsg `json:"messages"`
 | |
| }
 | |
| 
 | |
| //var firstMsgs chan myMsg
 | |
| //var myRooms map[string](chan myMsg)
 | |
| var msgHistory []myMsg
 | |
| var broadcastMsg chan myMsg
 | |
| 
 | |
| var newConns chan net.Conn
 | |
| var newTcpChat chan bufferedConn
 | |
| var authTcpChat chan tcpUser
 | |
| var delTcpChat chan bufferedConn
 | |
| var newHttpChat chan bufferedConn
 | |
| var newHttpClient chan bufferedConn
 | |
| var delHttpChat 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)
 | |
| }
 | |
| 
 | |
| // 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 * time.Millisecond)
 | |
| 					const msg = "Mailing auth code..."
 | |
| 					for _, r := range msg {
 | |
| 						time.Sleep(20 * time.Millisecond)
 | |
| 						fmt.Fprintf(bufConn, string(r))
 | |
| 					}
 | |
| 					time.Sleep(50 * time.Millisecond)
 | |
| 					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== TELNET AUTHORIZATION ==\n[cheat code for %s]: %s\n", email, code)
 | |
| 				time.Sleep(150 * time.Millisecond)
 | |
| 				fmt.Fprintf(bufConn, " done\n")
 | |
| 				time.Sleep(150 * time.Millisecond)
 | |
| 				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 * time.Millisecond)
 | |
| 				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
 | |
| 				close(u.userCount)
 | |
| 				u.userCount = nil
 | |
| 				time.Sleep(50 * time.Millisecond)
 | |
| 				fmt.Fprintf(bufConn, "\n")
 | |
| 				time.Sleep(50 * time.Millisecond)
 | |
| 				fmt.Fprintf(bufConn, "Welcome to #general (%d users)!", count)
 | |
| 				time.Sleep(50 * time.Millisecond)
 | |
| 				fmt.Fprintf(bufConn, "\n")
 | |
| 				time.Sleep(50 * time.Millisecond)
 | |
| 				// TODO /help /join <room> /users /channels /block <user> /upgrade <http/ws>
 | |
| 				//fmt.Fprintf(bufConn, "(TODO `/help' for list of commands)")
 | |
| 				time.Sleep(100 * time.Millisecond)
 | |
| 				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{
 | |
| 		broadcastMsg <- myMsg{
 | |
| 			ReceivedAt: time.Now(),
 | |
| 			sender:     bufConn,
 | |
| 			Message:    string(buf[0:count]),
 | |
| 			Channel:    "general",
 | |
| 			User:       email,
 | |
| 		}
 | |
| 		//fmt.Fprintf(bufConn, "> ")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func handleSorted(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: ")
 | |
| 		newTcpChat <- conn
 | |
| 		return
 | |
| 	} else if "HTTP" != protocol {
 | |
| 		defer conn.Close()
 | |
| 		fmt.Fprintf(conn, "\n\nNot yet supported. Try HTTP or Telnet\n\n")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	newHttpClient <- conn
 | |
| 
 | |
| 	/*
 | |
| 	   firstMsgs <- myMsg{
 | |
| 	     ReceivedAt: time.Now(),
 | |
| 	     sender: conn,
 | |
| 	     Message: 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{
 | |
| 	     broadcastMsg <- myMsg{
 | |
| 	       ReceivedAt: time.Now(),
 | |
| 	       sender: conn,
 | |
| 	       Message: string(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
 | |
| 
 | |
| 	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
 | |
| 			newHttpChat <- bufConn
 | |
| 		} else {
 | |
| 			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 (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)
 | |
| 	} else {
 | |
| 		fmt.Fprintf(os.Stdout, "[Mailgun] Status: %d", resp.StatusCode)
 | |
| 	}
 | |
| 
 | |
| 	return code, nil
 | |
| }
 | |
| 
 | |
| type myServer struct {
 | |
| 	chans chan bufferedConn
 | |
| 	net.Listener
 | |
| }
 | |
| 
 | |
| func (m *myServer) Accept() (net.Conn, error) {
 | |
| 	bufConn := <-m.chans
 | |
| 	return bufConn, nil
 | |
| }
 | |
| 
 | |
| func newMyServer(l net.Listener) *myServer {
 | |
| 	return &myServer{make(chan bufferedConn), l}
 | |
| }
 | |
| 
 | |
| var config Conf
 | |
| 
 | |
| func serveStatic(req *restful.Request, resp *restful.Response) {
 | |
| 	actual := path.Join(config.RootPath, req.PathParameter("subpath"))
 | |
| 	fmt.Printf("serving %s ... (from %s)\n", actual, req.PathParameter("subpath"))
 | |
| 	http.ServeFile(
 | |
| 		resp.ResponseWriter,
 | |
| 		req.Request,
 | |
| 		actual)
 | |
| }
 | |
| 
 | |
| func serveHello(req *restful.Request, resp *restful.Response) {
 | |
| 	fmt.Fprintf(resp, "{\"msg\":\"hello\"}")
 | |
| }
 | |
| 
 | |
| // TODO I probably should just make the non-exportable properties private/lowercase
 | |
| type authReq struct {
 | |
| 	Cid          string       `json:"cid"`
 | |
| 	ChallengedAt time.Time    `json:"-"`
 | |
| 	Chan         chan authReq `json:"-"`
 | |
| 	Otp          string       `json:"otp"`
 | |
| 	CreatedAt    time.Time    `json:"-"`
 | |
| 	DidAuth      bool         `json:"-"`
 | |
| 	Subject      string       `json:"sub"` // Subject as in 'sub' as per OIDC
 | |
| 	VerifiedAt   time.Time    `json:"-"`
 | |
| 	Tries        int          `json:"-"`
 | |
| }
 | |
| 
 | |
| func requestAuth(req *restful.Request, resp *restful.Response) {
 | |
| 	ar := authReq{
 | |
| 		CreatedAt: time.Now(),
 | |
| 		DidAuth:   false,
 | |
| 		Tries:     0,
 | |
| 	}
 | |
| 
 | |
| 	// Not sure why go restful finds it easier to do ReadEntity() than the "normal" way...
 | |
| 	// err := json.NewDecoder(req.Body).Decode(&ar)
 | |
| 	err := req.ReadEntity(&ar)
 | |
| 	if nil != err {
 | |
| 		fmt.Fprintf(resp, "{ \"error\": { \"message\": \"bad json in request body\"} }")
 | |
| 		return
 | |
| 	}
 | |
| 	email := strings.TrimSpace(ar.Subject)
 | |
| 	emailParts := strings.Split(email, "@")
 | |
| 	// TODO better pre-mailer validation (whitelist characters or use lib)
 | |
| 	if 2 != len(emailParts) {
 | |
| 		fmt.Fprintf(resp, "{ \"error\": { \"message\": \"bad email address '"+email+"'\"} }")
 | |
| 		return
 | |
| 	}
 | |
| 	ar.Subject = email
 | |
| 
 | |
| 	var otp string
 | |
| 	if "" != config.Mailer.ApiKey {
 | |
| 		otp, err = sendAuthCode(config.Mailer, ar.Subject)
 | |
| 		if nil != err {
 | |
| 			fmt.Fprintf(resp, "{ \"error\": { \"message\": \"error sending auth code via mailgun\" } }")
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 	if "" == otp {
 | |
| 		otp, err = genAuthCode()
 | |
| 		if nil != err {
 | |
| 			fmt.Fprintf(resp, "{ \"error\": { \"message\": \"error generating random number (code)\"} }")
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 	ar.Otp = otp
 | |
| 
 | |
| 	// Cheat code in case you didn't set up mailgun keys
 | |
| 	fmt.Fprintf(os.Stdout, "\n== HTTP AUTHORIZATION ==\n[cheat code for %s]: %s\n", ar.Subject, ar.Otp)
 | |
| 
 | |
| 	cid, _ := genAuthCode()
 | |
| 	if "" == cid {
 | |
| 		fmt.Fprintf(resp, "{ \"error\": { \"message\": \"error generating random number (cid)\"} }")
 | |
| 	}
 | |
| 	ar.Cid = cid
 | |
| 
 | |
| 	newAuthReqs <- ar
 | |
| 
 | |
| 	// Not sure why this works... technically there needs to be some sort of "end"
 | |
| 	// maybe it just figures that if I've returned
 | |
| 	fmt.Fprintf(resp, "{ \"success\": true, \"cid\": \""+ar.Cid+"\" }")
 | |
| }
 | |
| 
 | |
| func issueToken(req *restful.Request, resp *restful.Response) {
 | |
| 	ar := authReq{}
 | |
| 	cid := req.PathParameter("cid")
 | |
| 
 | |
| 	if "" == cid {
 | |
| 		fmt.Fprintf(resp, "{ \"error\": { \"message\": \"bad cid in request url params\"} }")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	//err := json.NewDecoder(r.Body).Decode(&ar)
 | |
| 	err := req.ReadEntity(&ar)
 | |
| 	if nil != err {
 | |
| 		fmt.Fprintf(resp, "{ \"error\": { \"message\": \"bad json in request body\"} }")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	ar.Cid = cid
 | |
| 	ar.Chan = make(chan authReq)
 | |
| 	valAuthReqs <- ar
 | |
| 	av := <-ar.Chan
 | |
| 	close(ar.Chan)
 | |
| 	ar.Chan = nil
 | |
| 	// TODO use a pointer instead?
 | |
| 	if "" == av.Otp {
 | |
| 		fmt.Fprintf(resp, "{ \"error\": { \"message\": \"invalid request: empty authorization challenge\"} }")
 | |
| 		return
 | |
| 	}
 | |
| 	av.Tries += 1
 | |
| 	av.ChallengedAt = time.Now()
 | |
| 
 | |
| 	// TODO security checks
 | |
| 	// * ChallengedAt was at least 1 second ago
 | |
| 	// * Tries does not exceed 5
 | |
| 	// * CreatedAt is not more than 15 minutes old
 | |
| 	// Probably also need to make sure than not more than n emails are sent per y minutes
 | |
| 
 | |
| 	// Not that this would even matter if the above were implemented, just a habit
 | |
| 	if 1 != subtle.ConstantTimeCompare([]byte(av.Otp), []byte(ar.Otp)) {
 | |
| 		fmt.Fprintf(resp, "{ \"error\": { \"message\": \"invalid authorization code\"} }")
 | |
| 		// I'm not sure if this is necessary, but I think it is
 | |
| 		// to overwrite the original with the updated
 | |
| 		// (these are copies, not pointers, IIRC)
 | |
| 		// and it seems like this is how I might write to a DB anyway
 | |
| 		newAuthReqs <- av
 | |
| 		return
 | |
| 	}
 | |
| 	av.DidAuth = true
 | |
| 	ar.VerifiedAt = time.Now()
 | |
| 	newAuthReqs <- av
 | |
| 
 | |
| 	// TODO I would use a JWT, but I need to wrap up this project
 | |
| 	fmt.Fprintf(resp, "{ \"success\": true, \"token\": \""+ar.Cid+"\" }")
 | |
| }
 | |
| 
 | |
| func requireToken(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
 | |
| 	ar := authReq{}
 | |
| 
 | |
| 	auth := req.HeaderParameter("Authorization")
 | |
| 	if "" == auth {
 | |
| 		fmt.Fprintf(resp, "{ \"error\": { \"message\": \"missing Authorization header\"} }")
 | |
| 		return
 | |
| 	}
 | |
| 	authParts := strings.Split(auth, " ")
 | |
| 	if "bearer" != strings.ToLower(authParts[0]) || "" == authParts[1] {
 | |
| 		fmt.Fprintf(resp, "{ \"error\": { \"message\": \"expected 'Authorization: Bearer <Token>'\"} }")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	ar.Cid = authParts[1]
 | |
| 	ar.Chan = make(chan authReq)
 | |
| 	valAuthReqs <- ar
 | |
| 	av := <-ar.Chan
 | |
| 	close(ar.Chan)
 | |
| 	ar.Chan = nil
 | |
| 	// TODO use a pointer instead?
 | |
| 	if "" == av.Cid {
 | |
| 		fmt.Fprintf(resp, "{ \"error\": { \"message\": \"invalid token: no session found\"} }")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// I prefer testing for "if not good" to "if bad"
 | |
| 	// (much safer in the dynamic world I come from)
 | |
| 	if true != av.DidAuth {
 | |
| 		fmt.Fprintf(resp, "{ \"error\": { \"message\": \"bad session'\"} }")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	req.SetAttribute("user", av.Subject)
 | |
| 	chain.ProcessFilter(req, resp)
 | |
| }
 | |
| func listMsgs(req *restful.Request, resp *restful.Response) {
 | |
| 	// TODO support ?since=<ISO_TS>
 | |
| 	// Also, data race? the list could be added to while this is iterating?
 | |
| 	// For now we'll just let the client sort the list
 | |
| 	resp.WriteEntity(&JsonMsg{
 | |
| 		Messages: msgHistory,
 | |
| 	})
 | |
| }
 | |
| func postMsg(req *restful.Request, resp *restful.Response) {
 | |
| 	user, ok := req.Attribute("user").(string)
 | |
| 	if !ok {
 | |
| 		fmt.Fprintf(resp, "{ \"error\": { \"code\": \"E_SANITY\", \"message\": \"SANITY FAIL user was not set, nor session error sent\"} }")
 | |
| 		return
 | |
| 	}
 | |
| 	if "" == user {
 | |
| 		fmt.Fprintf(resp, "{ \"error\": { \"code\": \"E_SESSION\", \"message\": \"invalid session\"} }")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	msg := myMsg{}
 | |
| 	err := req.ReadEntity(&msg)
 | |
| 	if nil != err {
 | |
| 		fmt.Fprintf(resp, "{ \"error\": { \"code\": \"E_FORMAT\", \"message\": \"invalid json POST\"} }")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	msg.sender = nil
 | |
| 	msg.ReceivedAt = time.Now()
 | |
| 	msg.User = user
 | |
| 	if "" == msg.Channel {
 | |
| 		msg.Channel = "general"
 | |
| 	}
 | |
| 	if "" == msg.Message {
 | |
| 		fmt.Fprintf(resp, "{ \"error\": { \"code\": \"E_FORMAT\", \"message\": \"please specify a 'message'\"} }")
 | |
| 		return
 | |
| 	}
 | |
| 	broadcastMsg <- msg
 | |
| 
 | |
| 	fmt.Fprintf(resp, "{ \"success\": true }")
 | |
| }
 | |
| 
 | |
| 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{}
 | |
| 	}
 | |
| 	if "" == config.RootPath {
 | |
| 		// TODO Embed the public dir at the default
 | |
| 		config.RootPath = "./public"
 | |
| 	}
 | |
| 
 | |
| 	// The magical sorting hat
 | |
| 	newConns = make(chan net.Conn, 128)
 | |
| 
 | |
| 	// TCP & Authentication
 | |
| 	myRawConns := make(map[bufferedConn]bool)
 | |
| 	newTcpChat = make(chan bufferedConn, 128)
 | |
| 	authTcpChat = 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)
 | |
| 	newHttpChat = make(chan bufferedConn, 128)
 | |
| 	newHttpClient = make(chan bufferedConn, 128)
 | |
| 
 | |
| 	// cruft to delete
 | |
| 	//myRooms = make(map[string](chan myMsg))
 | |
| 	//firstMsgs = make(chan myMsg, 128)
 | |
| 
 | |
| 	//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)
 | |
| 	msgHistory = make([]myMsg, 128)
 | |
| 	msgIndex := 0
 | |
| 
 | |
| 	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
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	// 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/general").Filter(requireToken).To(listMsgs))
 | |
| 	wsApi.Route(wsApi.POST("/rooms/general").Filter(requireToken).To(postMsg))
 | |
| 	container.Add(wsApi)
 | |
| 
 | |
| 	server := &http.Server{
 | |
| 		Addr:    addr,
 | |
| 		Handler: container,
 | |
| 	}
 | |
| 	myHttpServer := newMyServer(sock)
 | |
| 	go func() {
 | |
| 		server.Serve(myHttpServer)
 | |
| 	}()
 | |
| 
 | |
| 	// 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)
 | |
| 			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 {
 | |
| 				ar.Chan <- authReq{}
 | |
| 			}
 | |
| 		case ar := <-delAuthReqs:
 | |
| 			delete(myAuthReqs, ar.Cid)
 | |
| 		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 bufConn := <-newHttpClient:
 | |
| 			// this will be Accept()ed immediately by restful
 | |
| 			// NOTE: we don't store these HTTP connections for broadcast
 | |
| 			// as we manage the session by HTTP Auth Bearer rather than TCP
 | |
| 			myHttpServer.chans <- bufConn
 | |
| 		case msg := <-broadcastMsg:
 | |
| 			msgHistory[msgIndex] = msg
 | |
| 			msgIndex += 1
 | |
| 			msgIndex %= len(msgHistory)
 | |
| 
 | |
| 			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()
 | |
| 
 | |
| 			// TODO put logging here
 | |
| 			//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.User, msg.Message)
 | |
| 
 | |
| 			for conn, _ := range myRawConns {
 | |
| 				// Don't echo back to the original client
 | |
| 				if msg.sender == conn {
 | |
| 					continue
 | |
| 				}
 | |
| 
 | |
| 				// 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
 | |
| 				// TODO maybe use a chan to send to the socket's event loop
 | |
| 				go func(conn bufferedConn) {
 | |
| 					// 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.User, msg.Message)
 | |
| 					if nil != err {
 | |
| 						delTcpChat <- conn
 | |
| 					}
 | |
| 				}(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.Message)
 | |
| 			*/
 | |
| 		}
 | |
| 	}
 | |
| }
 |