| 
									
										
										
										
											2018-08-02 01:09:34 -06:00
										 |  |  | package main | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"crypto/subtle" | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							| 
									
										
										
										
											2018-08-02 02:13:56 -06:00
										 |  |  | 	"net" | 
					
						
							| 
									
										
										
										
											2018-08-02 01:09:34 -06:00
										 |  |  | 	"net/http" | 
					
						
							|  |  |  | 	"os" | 
					
						
							|  |  |  | 	"path" | 
					
						
							|  |  |  | 	"strings" | 
					
						
							|  |  |  | 	"time" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	restful "github.com/emicklei/go-restful" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-02 02:52:55 -06:00
										 |  |  | type JsonMsg struct { | 
					
						
							|  |  |  | 	Messages []*chatMsg `json:"messages"` | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-02 02:13:56 -06:00
										 |  |  | 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} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-02 01:09:34 -06:00
										 |  |  | // 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")) | 
					
						
							| 
									
										
										
										
											2018-08-02 02:52:55 -06:00
										 |  |  | 	http.ServeFile(resp.ResponseWriter, req.Request, actual) | 
					
						
							| 
									
										
										
										
											2018-08-02 01:09:34 -06:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-02 02:52:55 -06:00
										 |  |  | 	// Cheat code in case you didn't set up mailgun keys in the config file | 
					
						
							| 
									
										
										
										
											2018-08-02 01:09:34 -06:00
										 |  |  | 	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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-02 02:52:55 -06:00
										 |  |  | 	authReqs <- ar | 
					
						
							| 
									
										
										
										
											2018-08-02 01:09:34 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	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 | 
					
						
							| 
									
										
										
										
											2018-08-02 02:52:55 -06:00
										 |  |  | 		authReqs <- av | 
					
						
							| 
									
										
										
										
											2018-08-02 01:09:34 -06:00
										 |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	av.DidAuth = true | 
					
						
							|  |  |  | 	ar.VerifiedAt = time.Now() | 
					
						
							| 
									
										
										
										
											2018-08-02 02:52:55 -06:00
										 |  |  | 	authReqs <- av | 
					
						
							| 
									
										
										
										
											2018-08-02 01:09:34 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// 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) { | 
					
						
							| 
									
										
										
										
											2018-08-02 02:52:55 -06:00
										 |  |  | 	// TODO support ?since=<ISO_TS>, 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? | 
					
						
							| 
									
										
										
										
											2018-08-02 01:09:34 -06:00
										 |  |  | 	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 | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-02 02:25:10 -06:00
										 |  |  | 	msg := chatMsg{} | 
					
						
							| 
									
										
										
										
											2018-08-02 01:09:34 -06:00
										 |  |  | 	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 }") | 
					
						
							|  |  |  | } |