From b3ef87cbdde1bebaa4491e18580c21badf99d220 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 31 Jul 2018 20:35:40 -0600 Subject: [PATCH] WIP untested go restful APIs --- chatserver.go | 158 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 151 insertions(+), 7 deletions(-) diff --git a/chatserver.go b/chatserver.go index 4608843..5bd17b3 100644 --- a/chatserver.go +++ b/chatserver.go @@ -7,6 +7,7 @@ import ( "bufio" "bytes" "crypto/rand" + "crypto/subtle" "encoding/base64" "flag" "fmt" @@ -95,6 +96,9 @@ 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") @@ -228,6 +232,7 @@ func handleRaw(bufConn bufferedConn) { // 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") @@ -483,10 +488,137 @@ func serveStatic(req *restful.Request, resp *restful.Response) { req.Request, actual) } + func serveHello(req *restful.Request, resp *restful.Response) { fmt.Fprintf(resp, "{\"msg\":\"hello\"}") } +type authReq struct { + Cid string `json:"cid"` + ChallengedAt time.Time `json:"-"` + Chan chan authReq `json:"-"` + Code string `json:"code"` + CreatedAt time.Time `json:"-"` + DidAuth bool `json:"-"` + Email string `json:"email"` + VerifiedAt time.Time `json:"-"` + Tries int `json:"-"` +} + +func requestAuth(req *restful.Request, resp *restful.Response) { + ar := authReq{ + CreatedAt: time.Now(), + Tries: 0, + } + + err := req.ReadEntity(&ar) + // Looks like restful handles JSON automatically? + /* + err := json.NewDecoder(req.Body).Decode(&ar) + */ + if nil != err { + fmt.Fprintf(resp, "{ \"error\": { \"message\": \"bad json in request body\"} }") + return + } + email := strings.TrimSpace(ar.Email) + emailParts := strings.Split(email, "@") + if 2 != len(emailParts) { + fmt.Fprintf(resp, "{ \"error\": { \"message\": \"bad email address '"+email+"'\"} }") + return + } + + var code string + if "" != config.Mailer.ApiKey { + code, err = sendAuthCode(config.Mailer, email) + if nil != err { + fmt.Fprintf(resp, "{ \"error\": { \"message\": \"error sending auth code via mailgun\" } }") + return + } + } + if "" == code { + code, err = genAuthCode() + if nil != err { + fmt.Fprintf(resp, "{ \"error\": { \"message\": \"error generating random number (code)\"} }") + return + } + } + + fmt.Fprintf(os.Stdout, "\n== AUTHORIZATION ==\n[cheat code for %s]: %s\n", ar.Email, ar.Code) + + cid, _ := genAuthCode() + if "" == cid { + fmt.Fprintf(resp, "{ \"error\": { \"message\": \"error generating random number (cid)\"} }") + } + ar.Cid = cid + ar.DidAuth = false + ar.Email = email + ar.Code = code + + newAuthReqs <- ar + + 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 + 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 with the above, just a habit + if 1 != subtle.ConstantTimeCompare([]byte(av.Code), []byte(ar.Code)) { + 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) + // of course, should use a DB anyway... + newAuthReqs <- av + return + } + av.DidAuth = true + ar.VerifiedAt = time.Now() + newAuthReqs <- av + + fmt.Fprintf(resp, "{ \"success\": true, \"token\": \""+ar.Cid+"\" }") +} + +func requireToken(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) { + //req.SetAttribute("go", "there") + //req.Attribute("go") // "there" + chain.ProcessFilter(req, resp) +} +func listMsgs(req *restful.Request, resp *restful.Response) { + fmt.Fprintf(resp, "{ \"error\": { \"code\": \"E_NO_IMPL\", \"message\": \"invalid authorization code\"} }") +} +func postMsg(req *restful.Request, resp *restful.Response) { + fmt.Fprintf(resp, "{ \"error\": { \"code\": \"E_NO_IMPL\", \"message\": \"invalid authorization code\"} }") +} + func main() { flag.Usage = usage port := flag.Uint("telnet-port", 0, "tcp telnet chat port") @@ -509,10 +641,14 @@ func main() { } myRawConns := make(map[bufferedConn]bool) + myAuthReqs := make(map[string]authReq) firstMsgs = make(chan myMsg, 128) //myRooms = make(map[string](chan myMsg)) newConns = make(chan net.Conn, 128) authTcpChat = make(chan tcpUser, 128) + newAuthReqs = make(chan authReq, 128) + valAuthReqs = make(chan authReq, 128) + delAuthReqs = make(chan authReq, 128) newTcpChat = make(chan bufferedConn, 128) newHttpChat = make(chan bufferedConn, 128) newHttpClient = make(chan bufferedConn, 128) @@ -563,14 +699,14 @@ func main() { 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") - wsApi.Route(wsApi.GET("/api/hello").To(serveHello)) - /* - ws.Route(ws.POST("/api/authn").To(createAuth)) - ws.Route(ws.POST("/api/authn/{email}").To(createAuth)) - ws.Route(ws.GET("/api").Filter(basicAuthenticate).To(hello2)) - */ + 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{ @@ -602,6 +738,14 @@ func main() { channel: "general", email: "system", } + case ar := <-newAuthReqs: + myAuthReqs[ar.Cid] = ar + case av := <-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.Chan <- myAuthReqs[av.Cid] + case ar := <-delAuthReqs: + delete(myAuthReqs, ar.Cid) case bufConn := <-newTcpChat: go handleRaw(bufConn) case bufConn := <-delTcpChat: