241 lignes
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			241 lignes
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
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 <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>, 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 }")
 | 
						|
}
 |