229 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
		
		
			
		
	
	
			229 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
|  | 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 }") | ||
|  | } |