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