some file organization
This commit is contained in:
parent
a26cfccd19
commit
e7ae08c8fe
|
@ -0,0 +1,228 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
)
|
||||
|
||||
// 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 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\"}")
|
||||
}
|
||||
|
||||
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: myChatHist.msgs[:myChatHist.c],
|
||||
})
|
||||
}
|
||||
|
||||
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 }")
|
||||
}
|
|
@ -0,0 +1,200 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Trying to keep it slim with just one goroutine per client for each reads and writes.
|
||||
// Initially I was spawning a goroutine per write in the main select, but my guess is that
|
||||
// constantly allocating and cleaning up 4k of memory (or perhaps less these days
|
||||
// https://blog.nindalf.com/posts/how-goroutines-work/) is probably not very efficient for
|
||||
// small tweet-sized network writes. Also, I like this style better
|
||||
// TODO: Learn if it matters at all to have fewer long-lived vs more short-lived goroutines
|
||||
|
||||
// Auth & Reads
|
||||
func handleTelnetConn(bufConn bufferedConn) {
|
||||
// Used as a reference: 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)
|
||||
var u *tcpUser
|
||||
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")
|
||||
|
||||
if nil != u {
|
||||
delTcpChat <- *u
|
||||
}
|
||||
break
|
||||
}
|
||||
buf := buffer[:count]
|
||||
|
||||
// Rate Limit: Reasonable poor man's DoS prevention (Part 1)
|
||||
// A human does not send messages super fast and blocking the
|
||||
// writes of other incoming messages to this client for this long
|
||||
// won't hinder the user experience (and may in fact enhance it)
|
||||
// TODO: should do this for HTTP as well (or, better yet, implement hashcash)
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
|
||||
// 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, "[SANITY FAIL] using a 0-length buffer")
|
||||
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
|
||||
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),
|
||||
newMsg: make(chan string, 10), // reasonably sized
|
||||
}
|
||||
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
|
||||
|
||||
// Note: There's a 500ms gap between when we accept the client
|
||||
// and when it can start receiving messages and when it begins
|
||||
// to handle them, however, it's unlikely that >= 10 messages
|
||||
// will simultaneously flood in during that time
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
fmt.Fprintf(bufConn, "\n")
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
fmt.Fprintf(bufConn, "\033[1;32m"+"Welcome to #general (%d users)!"+"\033[22;39m", 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")
|
||||
|
||||
// Would be cool to write a prompt...
|
||||
// I wonder if I could send the correct ANSI codes for that...
|
||||
//fmt.Fprintf(bufConn, "\n%s> ", email)
|
||||
|
||||
go handleTelnetBroadcast(u)
|
||||
}
|
||||
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, "> ")
|
||||
}
|
||||
}
|
||||
|
||||
// Writes (post Auth)
|
||||
func handleTelnetBroadcast(u *tcpUser) {
|
||||
for {
|
||||
msg, more := <-u.newMsg
|
||||
if !more {
|
||||
// channel was closed
|
||||
break
|
||||
}
|
||||
|
||||
// Disallow Reverse Rate Limit: Reasonable poor man's DoS prevention (Part 3)
|
||||
// https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/
|
||||
timeoutDuration := 2 * time.Second
|
||||
u.bufConn.SetWriteDeadline(time.Now().Add(timeoutDuration))
|
||||
_, err := fmt.Fprintf(u.bufConn, msg)
|
||||
if nil != err {
|
||||
delTcpChat <- *u
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
414
chatserver.go
414
chatserver.go
|
@ -7,7 +7,6 @@ import (
|
|||
"bufio"
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
@ -17,7 +16,6 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -96,13 +94,14 @@ type chatHist struct {
|
|||
var myChatHist chatHist
|
||||
var broadcastMsg chan myMsg
|
||||
|
||||
var newConns chan net.Conn
|
||||
var virginConns chan net.Conn
|
||||
var wantsServerHello chan bufferedConn
|
||||
var authTcpChat chan tcpUser
|
||||
var delTcpChat chan tcpUser
|
||||
var gotClientHello chan bufferedConn
|
||||
|
||||
// Http
|
||||
var demuxHttpClient chan bufferedConn
|
||||
var delHttpChat chan bufferedConn
|
||||
var newAuthReqs chan authReq
|
||||
var valAuthReqs chan authReq
|
||||
var delAuthReqs chan authReq
|
||||
|
@ -127,193 +126,6 @@ func genAuthCode() (string, error) {
|
|||
return base64.URLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
// Trying to keep it slim with just one goroutine per client for reads and one goroutine per client for writes.
|
||||
// Initially I was spawning a goroutine per write, but my guess is that constantly allocating and cleaning up 4k
|
||||
// of memory (or perhaps less these days https://blog.nindalf.com/posts/how-goroutines-work/) is probably not
|
||||
// very efficient for small tweet-sized network writes
|
||||
|
||||
// Auth & Reads
|
||||
func handleTelnetConn(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)
|
||||
var u *tcpUser
|
||||
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")
|
||||
|
||||
if nil != u {
|
||||
delTcpChat <- *u
|
||||
}
|
||||
break
|
||||
}
|
||||
buf := buffer[:count]
|
||||
|
||||
// Rate Limit: Reasonable poor man's DoS prevention (Part 1)
|
||||
// A human does not send messages super fast and blocking the
|
||||
// writes of other incoming messages to this client for this long
|
||||
// won't hinder the user experience (and may in fact enhance it)
|
||||
// TODO: should do this for HTTP as well (or, better yet, implement hashcash)
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
|
||||
// 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, "[SANITY FAIL] using a 0-length buffer")
|
||||
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
|
||||
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),
|
||||
newMsg: make(chan string, 10), // reasonably sized
|
||||
}
|
||||
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
|
||||
|
||||
// Note: There's a 500ms gap between when we accept the client
|
||||
// and when it can start receiving messages and when it begins
|
||||
// to handle them, however, it's unlikely that >= 10 messages
|
||||
// will simultaneously flood in during that time
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
fmt.Fprintf(bufConn, "\n")
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
fmt.Fprintf(bufConn, "\033[1;32m"+"Welcome to #general (%d users)!"+"\033[22;39m", 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")
|
||||
|
||||
// Would be cool to write a prompt...
|
||||
// I wonder if I could send the correct ANSI codes for that...
|
||||
//fmt.Fprintf(bufConn, "\n%s> ", email)
|
||||
|
||||
go handleTelnetBroadcast(u)
|
||||
}
|
||||
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, "> ")
|
||||
}
|
||||
}
|
||||
|
||||
// Writes (post Auth)
|
||||
func handleTelnetBroadcast(u *tcpUser) {
|
||||
for {
|
||||
msg := <-u.newMsg
|
||||
// Disallow Reverse Rate Limit: Reasonable poor man's DoS prevention (Part 3)
|
||||
// https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/
|
||||
timeoutDuration := 2 * time.Second
|
||||
u.bufConn.SetWriteDeadline(time.Now().Add(timeoutDuration))
|
||||
_, err := fmt.Fprintf(u.bufConn, msg)
|
||||
if nil != err {
|
||||
delTcpChat <- *u
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func muxTcp(conn bufferedConn) {
|
||||
// Wish List for protocol detection
|
||||
// * PROXY protocol (and loop)
|
||||
|
@ -483,219 +295,6 @@ func newHttpServer(l net.Listener) *myHttpServer {
|
|||
|
||||
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: myChatHist.msgs[:myChatHist.c],
|
||||
})
|
||||
}
|
||||
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")
|
||||
|
@ -718,7 +317,7 @@ func main() {
|
|||
}
|
||||
|
||||
// The magical sorting hat
|
||||
newConns = make(chan net.Conn, 128)
|
||||
virginConns = make(chan net.Conn, 128)
|
||||
|
||||
// TCP & Authentication
|
||||
myRawConns := make(map[bufferedConn]tcpUser)
|
||||
|
@ -768,7 +367,7 @@ func main() {
|
|||
// Could a connection abort or end before it's handled?
|
||||
fmt.Fprintf(os.Stderr, "Error accepting connection:\n%s\n", err)
|
||||
}
|
||||
newConns <- conn
|
||||
virginConns <- conn
|
||||
}
|
||||
}()
|
||||
|
||||
|
@ -807,7 +406,7 @@ func main() {
|
|||
// Main event loop handling most access to shared data
|
||||
for {
|
||||
select {
|
||||
case conn := <-newConns:
|
||||
case conn := <-virginConns:
|
||||
// This is short lived
|
||||
go handleConnection(conn)
|
||||
case u := <-authTcpChat:
|
||||
|
@ -842,6 +441,7 @@ func main() {
|
|||
go handleTelnetConn(bufConn)
|
||||
case u := <-delTcpChat:
|
||||
// we can safely ignore this error, if any
|
||||
close(u.newMsg)
|
||||
u.bufConn.Close()
|
||||
delete(myRawConns, u.bufConn)
|
||||
case bufConn := <-gotClientHello:
|
||||
|
|
Loading…
Reference in New Issue