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