Compare commits

...

2 Commits

Author SHA1 Message Date
AJ ONeal e7ae08c8fe some file organization 2018-08-02 01:09:34 -06:00
AJ ONeal a26cfccd19 bugfix message history 2018-08-02 00:33:11 -06:00
3 changed files with 436 additions and 409 deletions

228
chatserver-http.go Normal file
View File

@ -0,0 +1,228 @@
package main
import (
"crypto/subtle"
"fmt"
"net/http"
"os"
"path"
"strings"
"time"
restful "github.com/emicklei/go-restful"
)
// 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
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
newAuthReqs <- ar
// Not sure why this works... technically there needs to be some sort of "end"
// maybe it just figures that if I've returned
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 := json.NewDecoder(r.Body).Decode(&ar)
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
newAuthReqs <- av
return
}
av.DidAuth = true
ar.VerifiedAt = time.Now()
newAuthReqs <- 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>
// Also, data race? the list could be added to while this is iterating?
// For now we'll just let the client sort the list
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 := myMsg{}
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 }")
}

200
chatserver-telnet.go Normal file
View File

@ -0,0 +1,200 @@
package main
import (
"fmt"
"io"
"os"
"strings"
"sync"
"time"
)
// Trying to keep it slim with just one goroutine per client for each reads and writes.
// Initially I was spawning a goroutine per write in the main select, but my guess is that
// constantly allocating and cleaning up 4k of memory (or perhaps less these days
// https://blog.nindalf.com/posts/how-goroutines-work/) is probably not very efficient for
// small tweet-sized network writes. Also, I like this style better
// TODO: Learn if it matters at all to have fewer long-lived vs more short-lived goroutines
// Auth & Reads
func handleTelnetConn(bufConn bufferedConn) {
// Used as a reference: https://jameshfisher.com/2017/04/18/golang-tcp-server.html
var email string
var code string
var authn bool
// Handle all subsequent packets
buffer := make([]byte, 1024)
var u *tcpUser
for {
//fmt.Fprintf(os.Stdout, "[raw] Waiting for message...\n")
count, err := bufConn.Read(buffer)
if nil != err {
if io.EOF != err {
fmt.Fprintf(os.Stderr, "Non-EOF socket error: %s\n", err)
}
fmt.Fprintf(os.Stdout, "Ending socket\n")
if nil != u {
delTcpChat <- *u
}
break
}
buf := buffer[:count]
// Rate Limit: Reasonable poor man's DoS prevention (Part 1)
// A human does not send messages super fast and blocking the
// writes of other incoming messages to this client for this long
// won't hinder the user experience (and may in fact enhance it)
// TODO: should do this for HTTP as well (or, better yet, implement hashcash)
time.Sleep(150 * time.Millisecond)
// Fun fact: if the buffer's current length (not capacity) is 0
// then the Read returns 0 without error
if 0 == count {
fmt.Fprintf(os.Stdout, "[SANITY FAIL] using a 0-length buffer")
break
}
if !authn {
if "" == email {
// Indeed telnet sends CRLF as part of the message
//fmt.Fprintf(os.Stdout, "buf{%s}\n", buf[:count])
// TODO use safer email testing
email = strings.TrimSpace(string(buf[:count]))
emailParts := strings.Split(email, "@")
if 2 != len(emailParts) {
fmt.Fprintf(bufConn, "Email: ")
continue
}
// Debugging any weird characters as part of the message (just CRLF)
//fmt.Fprintf(os.Stdout, "email: '%v'\n", []byte(email))
// Just for a fun little bit of puzzah
// Note: Reaction times are about 100ms
// Procesing times are about 250ms
// Right around 300ms is about when a person literally begins to get bored (begin context switching)
// Therefore any interaction should take longer than 100ms (time to register)
// and either engage the user or complete before reaching 300ms (not yet bored)
// This little ditty is meant to act as a psuedo-progress bar to engage the user
// Aside: a keystroke typically takes >=50s to type (probably closer to 200ms between words)
// https://stackoverflow.com/questions/22505698/what-is-a-typical-keypress-duration
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
time.Sleep(50 * time.Millisecond)
const msg = "Mailing auth code..."
for _, r := range msg {
time.Sleep(20 * time.Millisecond)
fmt.Fprintf(bufConn, string(r))
}
time.Sleep(50 * time.Millisecond)
wg.Done()
}()
if "" != config.Mailer.ApiKey {
wg.Add(1)
go func() {
code, err = sendAuthCode(config.Mailer, strings.TrimSpace(email))
wg.Done()
}()
} else {
code, err = genAuthCode()
}
wg.Wait()
if nil != err {
// TODO handle better
// (not sure why a random number would fail,
// but on a machine without internet the calls
// to mailgun APIs would fail)
panic(err)
}
// so I don't have to actually go check my email
fmt.Fprintf(os.Stdout, "\n== TELNET AUTHORIZATION ==\n[cheat code for %s]: %s\n", email, code)
time.Sleep(150 * time.Millisecond)
fmt.Fprintf(bufConn, " done\n")
time.Sleep(150 * time.Millisecond)
fmt.Fprintf(bufConn, "Auth Code: ")
continue
}
if code != strings.TrimSpace(string(buf[:count])) {
fmt.Fprintf(bufConn, "Incorrect Code\nAuth Code: ")
} else {
authn = true
time.Sleep(150 * time.Millisecond)
fmt.Fprintf(bufConn, "\n")
u = &tcpUser{
bufConn: bufConn,
email: email,
userCount: make(chan int, 1),
newMsg: make(chan string, 10), // reasonably sized
}
authTcpChat <- *u
// prevent data race on len(myRawConns)
// XXX (there can't be a race between these two lines, right?)
count := <-u.userCount
close(u.userCount)
u.userCount = nil
// Note: There's a 500ms gap between when we accept the client
// and when it can start receiving messages and when it begins
// to handle them, however, it's unlikely that >= 10 messages
// will simultaneously flood in during that time
time.Sleep(50 * time.Millisecond)
fmt.Fprintf(bufConn, "\n")
time.Sleep(50 * time.Millisecond)
fmt.Fprintf(bufConn, "\033[1;32m"+"Welcome to #general (%d users)!"+"\033[22;39m", count)
time.Sleep(50 * time.Millisecond)
fmt.Fprintf(bufConn, "\n")
time.Sleep(50 * time.Millisecond)
// TODO /help /join <room> /users /channels /block <user> /upgrade <http/ws>
//fmt.Fprintf(bufConn, "(TODO `/help' for list of commands)")
time.Sleep(100 * time.Millisecond)
fmt.Fprintf(bufConn, "\n")
// Would be cool to write a prompt...
// I wonder if I could send the correct ANSI codes for that...
//fmt.Fprintf(bufConn, "\n%s> ", email)
go handleTelnetBroadcast(u)
}
continue
}
//fmt.Fprintf(os.Stdout, "Queing message...\n")
//myRooms["general"] <- myMsg{
broadcastMsg <- myMsg{
ReceivedAt: time.Now(),
sender: bufConn,
Message: string(buf[0:count]),
Channel: "general",
User: email,
}
//fmt.Fprintf(bufConn, "> ")
}
}
// Writes (post Auth)
func handleTelnetBroadcast(u *tcpUser) {
for {
msg, more := <-u.newMsg
if !more {
// channel was closed
break
}
// Disallow Reverse Rate Limit: Reasonable poor man's DoS prevention (Part 3)
// https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/
timeoutDuration := 2 * time.Second
u.bufConn.SetWriteDeadline(time.Now().Add(timeoutDuration))
_, err := fmt.Fprintf(u.bufConn, msg)
if nil != err {
delTcpChat <- *u
break
}
}
}

View File

@ -7,7 +7,6 @@ import (
"bufio"
"bytes"
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"flag"
"fmt"
@ -17,7 +16,6 @@ import (
"net/http"
"net/url"
"os"
"path"
"strconv"
"strings"
"sync"
@ -96,13 +94,14 @@ type chatHist struct {
var myChatHist chatHist
var broadcastMsg chan myMsg
var newConns chan net.Conn
var virginConns chan net.Conn
var wantsServerHello chan bufferedConn
var authTcpChat chan tcpUser
var delTcpChat chan tcpUser
var gotClientHello chan bufferedConn
// Http
var demuxHttpClient chan bufferedConn
var delHttpChat chan bufferedConn
var newAuthReqs chan authReq
var valAuthReqs chan authReq
var delAuthReqs chan authReq
@ -127,193 +126,6 @@ func genAuthCode() (string, error) {
return base64.URLEncoding.EncodeToString(b), nil
}
// Trying to keep it slim with just one goroutine per client for reads and one goroutine per client for writes.
// Initially I was spawning a goroutine per write, but my guess is that constantly allocating and cleaning up 4k
// of memory (or perhaps less these days https://blog.nindalf.com/posts/how-goroutines-work/) is probably not
// very efficient for small tweet-sized network writes
// Auth & Reads
func handleTelnetConn(bufConn bufferedConn) {
// TODO
// What happens if this is being read from range
// when it's being added here (data race)?
// Should I use a channel here instead?
// TODO see https://jameshfisher.com/2017/04/18/golang-tcp-server.html
var email string
var code string
var authn bool
// Handle all subsequent packets
buffer := make([]byte, 1024)
var u *tcpUser
for {
//fmt.Fprintf(os.Stdout, "[raw] Waiting for message...\n")
count, err := bufConn.Read(buffer)
if nil != err {
if io.EOF != err {
fmt.Fprintf(os.Stderr, "Non-EOF socket error: %s\n", err)
}
fmt.Fprintf(os.Stdout, "Ending socket\n")
if nil != u {
delTcpChat <- *u
}
break
}
buf := buffer[:count]
// Rate Limit: Reasonable poor man's DoS prevention (Part 1)
// A human does not send messages super fast and blocking the
// writes of other incoming messages to this client for this long
// won't hinder the user experience (and may in fact enhance it)
// TODO: should do this for HTTP as well (or, better yet, implement hashcash)
time.Sleep(150 * time.Millisecond)
// Fun fact: if the buffer's current length (not capacity) is 0
// then the Read returns 0 without error
if 0 == count {
fmt.Fprintf(os.Stdout, "[SANITY FAIL] using a 0-length buffer")
break
}
if !authn {
if "" == email {
// Indeed telnet sends CRLF as part of the message
//fmt.Fprintf(os.Stdout, "buf{%s}\n", buf[:count])
// TODO use safer email testing
email = strings.TrimSpace(string(buf[:count]))
emailParts := strings.Split(email, "@")
if 2 != len(emailParts) {
fmt.Fprintf(bufConn, "Email: ")
continue
}
// Debugging any weird characters as part of the message (just CRLF)
//fmt.Fprintf(os.Stdout, "email: '%v'\n", []byte(email))
// Just for a fun little bit of puzzah
// Note: Reaction times are about 100ms
// Procesing times are about 250ms
// Right around 300ms is about when a person literally begins to get bored (begin context switching)
// Therefore any interaction should take longer than 100ms (time to register)
// and either engage the user or complete before reaching 300ms (not yet bored)
// This little ditty is meant to act as a psuedo-progress bar to engage the user
// Aside: a keystroke typically takes >=50s to type (probably closer to 200ms between words)
// https://stackoverflow.com/questions/22505698/what-is-a-typical-keypress-duration
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
time.Sleep(50 * time.Millisecond)
const msg = "Mailing auth code..."
for _, r := range msg {
time.Sleep(20 * time.Millisecond)
fmt.Fprintf(bufConn, string(r))
}
time.Sleep(50 * time.Millisecond)
wg.Done()
}()
if "" != config.Mailer.ApiKey {
wg.Add(1)
go func() {
code, err = sendAuthCode(config.Mailer, strings.TrimSpace(email))
wg.Done()
}()
} else {
code, err = genAuthCode()
}
wg.Wait()
if nil != err {
// TODO handle better
// (not sure why a random number would fail,
// but on a machine without internet the calls
// to mailgun APIs would fail)
panic(err)
}
// so I don't have to actually go check my email
fmt.Fprintf(os.Stdout, "\n== TELNET AUTHORIZATION ==\n[cheat code for %s]: %s\n", email, code)
time.Sleep(150 * time.Millisecond)
fmt.Fprintf(bufConn, " done\n")
time.Sleep(150 * time.Millisecond)
fmt.Fprintf(bufConn, "Auth Code: ")
continue
}
if code != strings.TrimSpace(string(buf[:count])) {
fmt.Fprintf(bufConn, "Incorrect Code\nAuth Code: ")
} else {
authn = true
time.Sleep(150 * time.Millisecond)
fmt.Fprintf(bufConn, "\n")
u = &tcpUser{
bufConn: bufConn,
email: email,
userCount: make(chan int, 1),
newMsg: make(chan string, 10), // reasonably sized
}
authTcpChat <- *u
// prevent data race on len(myRawConns)
// XXX (there can't be a race between these two lines, right?)
count := <-u.userCount
close(u.userCount)
u.userCount = nil
// Note: There's a 500ms gap between when we accept the client
// and when it can start receiving messages and when it begins
// to handle them, however, it's unlikely that >= 10 messages
// will simultaneously flood in during that time
time.Sleep(50 * time.Millisecond)
fmt.Fprintf(bufConn, "\n")
time.Sleep(50 * time.Millisecond)
fmt.Fprintf(bufConn, "\033[1;32m"+"Welcome to #general (%d users)!"+"\033[22;39m", count)
time.Sleep(50 * time.Millisecond)
fmt.Fprintf(bufConn, "\n")
time.Sleep(50 * time.Millisecond)
// TODO /help /join <room> /users /channels /block <user> /upgrade <http/ws>
//fmt.Fprintf(bufConn, "(TODO `/help' for list of commands)")
time.Sleep(100 * time.Millisecond)
fmt.Fprintf(bufConn, "\n")
// Would be cool to write a prompt...
// I wonder if I could send the correct ANSI codes for that...
//fmt.Fprintf(bufConn, "\n%s> ", email)
go handleTelnetBroadcast(u)
}
continue
}
//fmt.Fprintf(os.Stdout, "Queing message...\n")
//myRooms["general"] <- myMsg{
broadcastMsg <- myMsg{
ReceivedAt: time.Now(),
sender: bufConn,
Message: string(buf[0:count]),
Channel: "general",
User: email,
}
//fmt.Fprintf(bufConn, "> ")
}
}
// Writes (post Auth)
func handleTelnetBroadcast(u *tcpUser) {
for {
msg := <-u.newMsg
// Disallow Reverse Rate Limit: Reasonable poor man's DoS prevention (Part 3)
// https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/
timeoutDuration := 2 * time.Second
u.bufConn.SetWriteDeadline(time.Now().Add(timeoutDuration))
_, err := fmt.Fprintf(u.bufConn, msg)
if nil != err {
delTcpChat <- *u
break
}
}
}
func muxTcp(conn bufferedConn) {
// Wish List for protocol detection
// * PROXY protocol (and loop)
@ -483,219 +295,6 @@ func newHttpServer(l net.Listener) *myHttpServer {
var config Conf
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\"}")
}
// 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 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
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
newAuthReqs <- ar
// Not sure why this works... technically there needs to be some sort of "end"
// maybe it just figures that if I've returned
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 := json.NewDecoder(r.Body).Decode(&ar)
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
newAuthReqs <- av
return
}
av.DidAuth = true
ar.VerifiedAt = time.Now()
newAuthReqs <- 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>
// Also, data race? the list could be added to while this is iterating?
// For now we'll just let the client sort the list
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 := myMsg{}
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 }")
}
func main() {
flag.Usage = usage
port := flag.Uint("telnet-port", 0, "tcp telnet chat port")
@ -718,7 +317,7 @@ func main() {
}
// The magical sorting hat
newConns = make(chan net.Conn, 128)
virginConns = make(chan net.Conn, 128)
// TCP & Authentication
myRawConns := make(map[bufferedConn]tcpUser)
@ -744,7 +343,6 @@ func main() {
broadcastMsg = make(chan myMsg, 128)
// Poor-Man's container/ring (circular buffer)
myChatHist.msgs = make([]*myMsg, 128)
msgIndex := 0
var addr string
if 0 != int(*port) {
@ -769,7 +367,7 @@ func main() {
// Could a connection abort or end before it's handled?
fmt.Fprintf(os.Stderr, "Error accepting connection:\n%s\n", err)
}
newConns <- conn
virginConns <- conn
}
}()
@ -808,7 +406,7 @@ func main() {
// Main event loop handling most access to shared data
for {
select {
case conn := <-newConns:
case conn := <-virginConns:
// This is short lived
go handleConnection(conn)
case u := <-authTcpChat:
@ -843,6 +441,7 @@ func main() {
go handleTelnetConn(bufConn)
case u := <-delTcpChat:
// we can safely ignore this error, if any
close(u.newMsg)
u.bufConn.Close()
delete(myRawConns, u.bufConn)
case bufConn := <-gotClientHello:
@ -854,7 +453,7 @@ func main() {
myHttpServer.chans <- bufConn
case msg := <-broadcastMsg:
// copy comes in, pointer gets saved (and not GC'd, I hope)
myChatHist.msgs[msgIndex] = &msg
myChatHist.msgs[myChatHist.i] = &msg
myChatHist.i += 1
if myChatHist.c < cap(myChatHist.msgs) {
myChatHist.c += 1