GEFN (good enough for now)

This commit is contained in:
AJ ONeal 2018-08-02 02:52:55 -06:00
parent 2b88da7a71
commit 3943c14845
3 changed files with 51 additions and 72 deletions

View File

@ -13,6 +13,10 @@ import (
restful "github.com/emicklei/go-restful" restful "github.com/emicklei/go-restful"
) )
type JsonMsg struct {
Messages []*chatMsg `json:"messages"`
}
type myHttpServer struct { type myHttpServer struct {
chans chan bufferedConn chans chan bufferedConn
net.Listener net.Listener
@ -43,10 +47,7 @@ type authReq struct {
func serveStatic(req *restful.Request, resp *restful.Response) { func serveStatic(req *restful.Request, resp *restful.Response) {
actual := path.Join(config.RootPath, req.PathParameter("subpath")) actual := path.Join(config.RootPath, req.PathParameter("subpath"))
fmt.Printf("serving %s ... (from %s)\n", actual, req.PathParameter("subpath")) fmt.Printf("serving %s ... (from %s)\n", actual, req.PathParameter("subpath"))
http.ServeFile( http.ServeFile(resp.ResponseWriter, req.Request, actual)
resp.ResponseWriter,
req.Request,
actual)
} }
func serveHello(req *restful.Request, resp *restful.Response) { func serveHello(req *restful.Request, resp *restful.Response) {
@ -93,7 +94,7 @@ func requestAuth(req *restful.Request, resp *restful.Response) {
} }
ar.Otp = otp ar.Otp = otp
// Cheat code in case you didn't set up mailgun keys // Cheat code in case you didn't set up mailgun keys in the config file
fmt.Fprintf(os.Stdout, "\n== HTTP AUTHORIZATION ==\n[cheat code for %s]: %s\n", ar.Subject, ar.Otp) fmt.Fprintf(os.Stdout, "\n== HTTP AUTHORIZATION ==\n[cheat code for %s]: %s\n", ar.Subject, ar.Otp)
cid, _ := genAuthCode() cid, _ := genAuthCode()
@ -102,10 +103,8 @@ func requestAuth(req *restful.Request, resp *restful.Response) {
} }
ar.Cid = cid ar.Cid = cid
newAuthReqs <- ar authReqs <- 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+"\" }") fmt.Fprintf(resp, "{ \"success\": true, \"cid\": \""+ar.Cid+"\" }")
} }
@ -118,7 +117,6 @@ func issueToken(req *restful.Request, resp *restful.Response) {
return return
} }
//err := json.NewDecoder(r.Body).Decode(&ar)
err := req.ReadEntity(&ar) err := req.ReadEntity(&ar)
if nil != err { if nil != err {
fmt.Fprintf(resp, "{ \"error\": { \"message\": \"bad json in request body\"} }") fmt.Fprintf(resp, "{ \"error\": { \"message\": \"bad json in request body\"} }")
@ -152,12 +150,12 @@ func issueToken(req *restful.Request, resp *restful.Response) {
// to overwrite the original with the updated // to overwrite the original with the updated
// (these are copies, not pointers, IIRC) // (these are copies, not pointers, IIRC)
// and it seems like this is how I might write to a DB anyway // and it seems like this is how I might write to a DB anyway
newAuthReqs <- av authReqs <- av
return return
} }
av.DidAuth = true av.DidAuth = true
ar.VerifiedAt = time.Now() ar.VerifiedAt = time.Now()
newAuthReqs <- av authReqs <- av
// TODO I would use a JWT, but I need to wrap up this project // TODO I would use a JWT, but I need to wrap up this project
fmt.Fprintf(resp, "{ \"success\": true, \"token\": \""+ar.Cid+"\" }") fmt.Fprintf(resp, "{ \"success\": true, \"token\": \""+ar.Cid+"\" }")
@ -201,9 +199,8 @@ func requireToken(req *restful.Request, resp *restful.Response, chain *restful.F
} }
func listMsgs(req *restful.Request, resp *restful.Response) { func listMsgs(req *restful.Request, resp *restful.Response) {
// TODO support ?since=<ISO_TS> // TODO support ?since=<ISO_TS>, but for now we'll just let the client sort the list
// Also, data race? the list could be added to while this is iterating? // TODO Could this have a data race if the list were added to while this is iterating?
// For now we'll just let the client sort the list
resp.WriteEntity(&JsonMsg{ resp.WriteEntity(&JsonMsg{
Messages: myChatHist.msgs[:myChatHist.c], Messages: myChatHist.msgs[:myChatHist.c],
}) })

View File

@ -9,6 +9,13 @@ import (
"time" "time"
) )
type telnetUser struct {
bufConn bufferedConn
userCount chan int
email string
newMsg chan string
}
// Trying to keep it slim with just one goroutine per client for each reads and writes. // 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 // 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 // constantly allocating and cleaning up 4k of memory (or perhaps less these days
@ -26,7 +33,7 @@ func handleTelnetConn(bufConn bufferedConn) {
// Handle all subsequent packets // Handle all subsequent packets
buffer := make([]byte, 1024) buffer := make([]byte, 1024)
var u *tcpUser var u *telnetUser
for { for {
//fmt.Fprintf(os.Stdout, "[raw] Waiting for message...\n") //fmt.Fprintf(os.Stdout, "[raw] Waiting for message...\n")
count, err := bufConn.Read(buffer) count, err := bufConn.Read(buffer)
@ -136,7 +143,7 @@ func handleTelnetConn(bufConn bufferedConn) {
authn = true authn = true
time.Sleep(150 * time.Millisecond) time.Sleep(150 * time.Millisecond)
fmt.Fprintf(bufConn, "\n") fmt.Fprintf(bufConn, "\n")
u = &tcpUser{ u = &telnetUser{
bufConn: bufConn, bufConn: bufConn,
email: email, email: email,
userCount: make(chan int, 1), userCount: make(chan int, 1),
@ -156,6 +163,7 @@ func handleTelnetConn(bufConn bufferedConn) {
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
fmt.Fprintf(bufConn, "\n") fmt.Fprintf(bufConn, "\n")
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
// It turns out that ANSI characters work in Telnet just fine
fmt.Fprintf(bufConn, "\033[1;32m"+"Welcome to #general (%d users)!"+"\033[22;39m", count) fmt.Fprintf(bufConn, "\033[1;32m"+"Welcome to #general (%d users)!"+"\033[22;39m", count)
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
fmt.Fprintf(bufConn, "\n") fmt.Fprintf(bufConn, "\n")
@ -166,7 +174,8 @@ func handleTelnetConn(bufConn bufferedConn) {
fmt.Fprintf(bufConn, "\n") fmt.Fprintf(bufConn, "\n")
// Would be cool to write a prompt... // Would be cool to write a prompt...
// I wonder if I could send the correct ANSI codes for that... // I wonder if I could send fudge some ANSI codes to keep the prompt
// even when new messages come in, but not overwrite what he user typed...
//fmt.Fprintf(bufConn, "\n%s> ", email) //fmt.Fprintf(bufConn, "\n%s> ", email)
go handleTelnetBroadcast(u) go handleTelnetBroadcast(u)
@ -185,7 +194,7 @@ func handleTelnetConn(bufConn bufferedConn) {
} }
// Writes (post Auth) // Writes (post Auth)
func handleTelnetBroadcast(u *tcpUser) { func handleTelnetBroadcast(u *telnetUser) {
for { for {
msg, more := <-u.newMsg msg, more := <-u.newMsg
if !more { if !more {

View File

@ -39,13 +39,6 @@ type ConfMailer struct {
From string `yaml:"from,omitempty"` From string `yaml:"from,omitempty"`
} }
type tcpUser struct {
bufConn bufferedConn
userCount chan int
email string
newMsg chan string
}
// So we can peek at net.Conn, which we can't do natively // 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 // https://stackoverflow.com/questions/51472020/how-to-get-the-size-of-available-tcp-data
type bufferedConn struct { type bufferedConn struct {
@ -80,28 +73,29 @@ type chatMsg struct {
Channel string `json:"channel"` Channel string `json:"channel"`
User string `json:"user"` User string `json:"user"`
} }
type JsonMsg struct {
Messages []*chatMsg `json:"messages"`
}
// Poor-Man's container/ring (circular buffer)
type chatHist struct { type chatHist struct {
msgs []*chatMsg msgs []*chatMsg
i int i int // current index
c int 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 myChatHist chatHist
var broadcastMsg chan chatMsg var broadcastMsg chan chatMsg
var virginConns chan net.Conn // Telnet
var wantsServerHello chan bufferedConn var wantsServerHello chan bufferedConn
var authTelnet chan tcpUser var authTelnet chan telnetUser
var cleanTelnet chan tcpUser var cleanTelnet chan telnetUser
var gotClientHello chan bufferedConn
// HTTP // HTTP
var demuxHttpClient chan bufferedConn var demuxHttpClient chan bufferedConn
var newAuthReqs chan authReq var authChallenge chan authReq
var valAuthReqs chan authReq var valAuthReqs chan authReq
var delAuthReqs chan authReq var delAuthReqs chan authReq
@ -184,7 +178,7 @@ func muxTcp(conn bufferedConn) {
demuxHttpClient <- conn demuxHttpClient <- conn
} }
func handleConnection(netConn net.Conn) { func testForHello(netConn net.Conn) {
ts := time.Now() ts := time.Now()
fmt.Fprintf(os.Stdout, "[New Connection] (%s) welcome %s\n", ts, netConn.RemoteAddr().String()) fmt.Fprintf(os.Stdout, "[New Connection] (%s) welcome %s\n", ts, netConn.RemoteAddr().String())
@ -193,13 +187,11 @@ func handleConnection(netConn net.Conn) {
bufConn := newBufferedConn(netConn) bufConn := newBufferedConn(netConn)
go func() { go func() {
// Handle First Packet // Cause first packet to be loaded into buffer
_, err := bufConn.Peek(1) _, err := bufConn.Peek(1)
//fmsg, err := bufConn.Peek(1)
if nil != err { if nil != err {
panic(err) panic(err)
} }
//fmt.Fprintf(os.Stdout, "[First Byte] %s\n", fmsg)
m.Lock() m.Lock()
if virgin { if virgin {
@ -211,14 +203,17 @@ func handleConnection(netConn net.Conn) {
m.Unlock() 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) time.Sleep(250 * 1000000)
// If we still haven't received data from the client // If we still haven't received data from the client
// assume that the client must be expecting a welcome from us // assume that the client must be expecting a welcome from us
m.Lock() m.Lock()
if virgin { if virgin {
virgin = false virgin = false
// don't block for this // Defer as to not block and prolonging the mutex
// let it be handled after the unlock // (not that those few cycles much matter...)
defer fmt.Fprintf(netConn, defer fmt.Fprintf(netConn,
"\n\nWelcome to Sample Chat! You appear to be using Telnet (http is also available on this port)."+ "\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: ") "\nYou must authenticate via email to participate\n\nEmail: ")
@ -251,7 +246,7 @@ func sendAuthCode(cnf ConfMailer, to string) (string, error) {
if nil != err { if nil != err {
return "", err return "", err
} }
//req.PostForm = form //req.PostForm = form ??
req.Header.Add("User-Agent", "golang http.Client - Sample Chat App Authenticator") req.Header.Add("User-Agent", "golang http.Client - Sample Chat App Authenticator")
req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.SetBasicAuth("api", cnf.ApiKey) req.SetBasicAuth("api", cnf.ApiKey)
@ -276,8 +271,6 @@ func sendAuthCode(cnf ConfMailer, to string) (string, error) {
return code, nil return code, nil
} }
var config Conf
func main() { func main() {
flag.Usage = usage flag.Usage = usage
port := flag.Uint("port", 0, "tcp telnet chat port") port := flag.Uint("port", 0, "tcp telnet chat port")
@ -295,7 +288,8 @@ func main() {
config = Conf{} config = Conf{}
} }
if "" == config.RootPath { if "" == config.RootPath {
// TODO Embed the public dir at the default // TODO Maybe embed the public dir into the binary
// (and provide a flag with path for override - like gitea)
config.RootPath = "./public" config.RootPath = "./public"
} }
@ -303,28 +297,24 @@ func main() {
virginConns = make(chan net.Conn, 128) virginConns = make(chan net.Conn, 128)
// TCP & Authentication // TCP & Authentication
telnetConns := make(map[bufferedConn]tcpUser) telnetConns := make(map[bufferedConn]telnetUser)
wantsServerHello = make(chan bufferedConn, 128) wantsServerHello = make(chan bufferedConn, 128)
authTelnet = make(chan tcpUser, 128) authTelnet = make(chan telnetUser, 128)
// HTTP & Authentication // HTTP & Authentication
myAuthReqs := make(map[string]authReq) myAuthReqs := make(map[string]authReq)
newAuthReqs = make(chan authReq, 128) authReqs = make(chan authReq, 128)
valAuthReqs = make(chan authReq, 128) valAuthReqs = make(chan authReq, 128)
delAuthReqs = make(chan authReq, 128) delAuthReqs = make(chan authReq, 128)
gotClientHello = make(chan bufferedConn, 128) gotClientHello = make(chan bufferedConn, 128)
demuxHttpClient = make(chan bufferedConn, 128) demuxHttpClient = make(chan bufferedConn, 128)
// cruft to delete
//myRooms = make(map[string](chan chatMsg))
//myRooms["general"] = make(chan chatMsg, 128) //myRooms["general"] = make(chan chatMsg, 128)
// Note: I had considered dynamically select on channels for rooms. // Note: I had considered dynamically select on channels for rooms.
// https://stackoverflow.com/questions/19992334/how-to-listen-to-n-channels-dynamic-select-statement // 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 // I don't think that's actually the best approach, but I just wanted to save the link
broadcastMsg = make(chan chatMsg, 128) broadcastMsg = make(chan chatMsg, 128)
// Poor-Man's container/ring (circular buffer)
myChatHist.msgs = make([]*chatMsg, 128) myChatHist.msgs = make([]*chatMsg, 128)
var addr string var addr string
@ -391,7 +381,7 @@ func main() {
select { select {
case conn := <-virginConns: case conn := <-virginConns:
// This is short lived // This is short lived
go handleConnection(conn) go testForHello(conn)
case u := <-authTelnet: case u := <-authTelnet:
// allow to receive messages // allow to receive messages
// (and be counted among the users) // (and be counted among the users)
@ -405,7 +395,7 @@ func main() {
Channel: "general", Channel: "general",
User: "system", User: "system",
} }
case ar := <-newAuthReqs: case ar := <-authReqs:
myAuthReqs[ar.Cid] = ar myAuthReqs[ar.Cid] = ar
case ar := <-valAuthReqs: case ar := <-valAuthReqs:
// TODO In this case it's probably more conventional (and efficient) to // TODO In this case it's probably more conventional (and efficient) to
@ -455,7 +445,7 @@ func main() {
sender = "system" sender = "system"
} }
// Tangential thought: // Tangential thought:
// I wonder if we could use IP detection to get the client's tz // 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 // ... could probably make time for this in the authentication loop
zone, _ := msg.ReceivedAt.Zone() zone, _ := msg.ReceivedAt.Zone()
fmt.Fprintf(os.Stdout, tf+" [%s] (%s): %s\r\n", fmt.Fprintf(os.Stdout, tf+" [%s] (%s): %s\r\n",
@ -484,23 +474,6 @@ func main() {
// It can reconnect. // It can reconnect.
cleanTelnet <- u cleanTelnet <- u
} }
/*
// To ask: Why do I have to pass in conn to prevent a data race? Is it garbage collection?
// Don't block the rest of the loop
// TODONE maybe use a chan to send to the socket's event loop
// (left this in to remind myself to ask questions)
go func(conn bufferedConn) {
// Protect against malicious clients to prevent DoS
// https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/
timeoutDuration := 2 * time.Second
conn.SetWriteDeadline(time.Now().Add(timeoutDuration))
_, err := fmt.Fprintf(conn, msg)
if nil != err {
cleanTelnet <- u
}
}(conn)
*/
} }
} }
} }