An example chat server in golang.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

240 lines
6.6 KiB

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 }")
}