diff --git a/chatserver-http.go b/chatserver-http.go new file mode 100644 index 0000000..cee83b3 --- /dev/null +++ b/chatserver-http.go @@ -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 '\"} }") + 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= + // 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 }") +} diff --git a/chatserver-telnet.go b/chatserver-telnet.go new file mode 100644 index 0000000..ec69b2c --- /dev/null +++ b/chatserver-telnet.go @@ -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 /users /channels /block /upgrade + //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 + } + } +} diff --git a/chatserver.go b/chatserver.go index ed848b3..95df8d4 100644 --- a/chatserver.go +++ b/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 /users /channels /block /upgrade - //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 '\"} }") - 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= - // 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: