package main import ( "crypto/subtle" "fmt" "net" "net/http" "os" "path" "strings" "time" restful "github.com/emicklei/go-restful" ) type JsonMsg struct { Messages []*chatMsg `json:"messages"` } type myHttpServer struct { chans chan bufferedConn net.Listener } func (m *myHttpServer) Accept() (net.Conn, error) { bufConn := <-m.chans return bufConn, nil } func newHttpServer(l net.Listener) *myHttpServer { return &myHttpServer{make(chan bufferedConn), l} } // 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 in the config file 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 authReqs <- 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 := 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 authReqs <- av return } av.DidAuth = true ar.VerifiedAt = time.Now() authReqs <- 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=, but for now we'll just let the client sort the list // TODO Could this have a data race if the list were added to while this is iterating? 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 := chatMsg{} 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 }") }