mostly just cleanup I think...
This commit is contained in:
parent
34ee3924d3
commit
64425b41d2
216
chatserver.go
216
chatserver.go
|
@ -36,6 +36,12 @@ type ConfMailer struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type tcpUser struct {
|
||||||
|
bufConn bufferedConn
|
||||||
|
userCount chan int
|
||||||
|
email string
|
||||||
|
}
|
||||||
|
|
||||||
// So we can peek at net.Conn, which we can't do natively
|
// So we can peek at net.Conn, which we can't do natively
|
||||||
// https://stackoverflow.com/questions/51472020/how-to-get-the-size-of-available-tcp-data
|
// https://stackoverflow.com/questions/51472020/how-to-get-the-size-of-available-tcp-data
|
||||||
type bufferedConn struct {
|
type bufferedConn struct {
|
||||||
|
@ -70,15 +76,16 @@ type myMsg struct {
|
||||||
bytes []byte
|
bytes []byte
|
||||||
receivedAt time.Time
|
receivedAt time.Time
|
||||||
channel string
|
channel string
|
||||||
|
email string
|
||||||
}
|
}
|
||||||
|
|
||||||
var firstMsgs chan myMsg
|
var firstMsgs chan myMsg
|
||||||
var myChans map[string](chan myMsg)
|
//var myRooms map[string](chan myMsg)
|
||||||
var myMsgs chan myMsg
|
var myMsgs chan myMsg
|
||||||
var myUnsortedConns map[net.Conn]bool
|
//var myUnsortedConns map[net.Conn]bool
|
||||||
var myRawConns map[net.Conn]bool
|
|
||||||
var newConns chan net.Conn
|
var newConns chan net.Conn
|
||||||
var newTcpChat chan bufferedConn
|
var newTcpChat chan bufferedConn
|
||||||
|
var authTcpChat chan tcpUser
|
||||||
var delTcpChat chan bufferedConn
|
var delTcpChat chan bufferedConn
|
||||||
var newHttpChat chan bufferedConn
|
var newHttpChat chan bufferedConn
|
||||||
var delHttpChat chan bufferedConn
|
var delHttpChat chan bufferedConn
|
||||||
|
@ -117,7 +124,7 @@ func handleRaw(bufConn bufferedConn) {
|
||||||
// Handle all subsequent packets
|
// Handle all subsequent packets
|
||||||
buffer := make([]byte, 1024)
|
buffer := make([]byte, 1024)
|
||||||
for {
|
for {
|
||||||
fmt.Fprintf(os.Stdout, "[raw] Waiting for message...\n");
|
//fmt.Fprintf(os.Stdout, "[raw] Waiting for message...\n");
|
||||||
count, err := bufConn.Read(buffer)
|
count, err := bufConn.Read(buffer)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
if io.EOF != err {
|
if io.EOF != err {
|
||||||
|
@ -125,7 +132,6 @@ func handleRaw(bufConn bufferedConn) {
|
||||||
}
|
}
|
||||||
fmt.Fprintf(os.Stdout, "Ending socket\n")
|
fmt.Fprintf(os.Stdout, "Ending socket\n")
|
||||||
|
|
||||||
// TODO put this in a channel to prevent data races
|
|
||||||
delTcpChat <- bufConn
|
delTcpChat <- bufConn
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -140,7 +146,9 @@ func handleRaw(bufConn bufferedConn) {
|
||||||
|
|
||||||
if !authn {
|
if !authn {
|
||||||
if "" == email {
|
if "" == email {
|
||||||
fmt.Fprintf(os.Stdout, "buf{%s}\n", buf[:count])
|
// Indeed telnet sends CRLF as part of the message
|
||||||
|
//fmt.Fprintf(os.Stdout, "buf{%s}\n", buf[:count])
|
||||||
|
|
||||||
// TODO use safer email testing
|
// TODO use safer email testing
|
||||||
email = strings.TrimSpace(string(buf[:count]))
|
email = strings.TrimSpace(string(buf[:count]))
|
||||||
emailParts := strings.Split(email, "@")
|
emailParts := strings.Split(email, "@")
|
||||||
|
@ -148,12 +156,53 @@ func handleRaw(bufConn bufferedConn) {
|
||||||
fmt.Fprintf(bufConn, "Email: ")
|
fmt.Fprintf(bufConn, "Email: ")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fmt.Fprintf(os.Stdout, "email: '%v'\n", []byte(email))
|
|
||||||
|
// 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
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
time.Sleep(50 * 1000000)
|
||||||
|
const msg = "Mailing auth code..."
|
||||||
|
for _, r := range msg {
|
||||||
|
time.Sleep(20 * 1000000)
|
||||||
|
fmt.Fprintf(bufConn, string(r))
|
||||||
|
}
|
||||||
|
time.Sleep(50 * 1000000)
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
if "" != config.Mailer.ApiKey {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
code, err = sendAuthCode(config.Mailer, strings.TrimSpace(email))
|
code, err = sendAuthCode(config.Mailer, strings.TrimSpace(email))
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
code, err = genAuthCode()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
if nil != err {
|
if nil != err {
|
||||||
// TODO handle better
|
// 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)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
// so I don't have to actually go check my email
|
||||||
|
fmt.Fprintf(os.Stdout, "\n== AUTHORIZATION ==\n[cheat code for %s]: %s\n", email, code)
|
||||||
|
time.Sleep(150 * 1000000)
|
||||||
|
fmt.Fprintf(bufConn, " done\n")
|
||||||
|
time.Sleep(150 * 1000000)
|
||||||
fmt.Fprintf(bufConn, "Auth Code: ")
|
fmt.Fprintf(bufConn, "Auth Code: ")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -162,27 +211,64 @@ func handleRaw(bufConn bufferedConn) {
|
||||||
fmt.Fprintf(bufConn, "Incorrect Code\nAuth Code: ")
|
fmt.Fprintf(bufConn, "Incorrect Code\nAuth Code: ")
|
||||||
} else {
|
} else {
|
||||||
authn = true
|
authn = true
|
||||||
fmt.Fprintf(bufConn, "Welcome to #general! (TODO `/help' for list of commands)\n")
|
time.Sleep(150 * 1000000)
|
||||||
// TODO number of users
|
fmt.Fprintf(bufConn, "\n")
|
||||||
//fmt.Fprintf(bufConn, "Welcome to #general! TODO `/list' to see channels. `/join chname' to switch.\n")
|
u := tcpUser{
|
||||||
|
bufConn: bufConn,
|
||||||
|
email: email,
|
||||||
|
userCount: make(chan int, 1),
|
||||||
|
}
|
||||||
|
authTcpChat <- u
|
||||||
|
// prevent data race on len(myRawConns)
|
||||||
|
// XXX (there can't be a race between these two lines, right?)
|
||||||
|
count := <- u.userCount
|
||||||
|
u.userCount = nil
|
||||||
|
time.Sleep(50 * 1000000)
|
||||||
|
fmt.Fprintf(bufConn, "\n")
|
||||||
|
time.Sleep(50 * 1000000)
|
||||||
|
fmt.Fprintf(bufConn, "Welcome to #general (%d users)!", count)
|
||||||
|
time.Sleep(50 * 1000000)
|
||||||
|
fmt.Fprintf(bufConn, "\n")
|
||||||
|
time.Sleep(50 * 1000000)
|
||||||
|
// TODO /help /join <room> /users /channels /block <user> /upgrade <http/ws>
|
||||||
|
//fmt.Fprintf(bufConn, "(TODO `/help' for list of commands)")
|
||||||
|
time.Sleep(100 * 1000000)
|
||||||
|
fmt.Fprintf(bufConn, "\n")
|
||||||
|
|
||||||
|
// this would be cool, but won't work since other messages will come
|
||||||
|
// in before the person responds
|
||||||
|
//fmt.Fprintf(bufConn, "\n%s> ", email)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(os.Stdout, "Queing message...\n");
|
//fmt.Fprintf(os.Stdout, "Queing message...\n");
|
||||||
//myChans["general"] <- myMsg{
|
//myRooms["general"] <- myMsg{
|
||||||
myMsgs <- myMsg{
|
myMsgs <- myMsg{
|
||||||
receivedAt: time.Now(),
|
receivedAt: time.Now(),
|
||||||
sender: bufConn,
|
sender: bufConn,
|
||||||
bytes: buf[0:count],
|
bytes: buf[0:count],
|
||||||
channel: "general",
|
channel: "general",
|
||||||
|
email: email,
|
||||||
}
|
}
|
||||||
|
//fmt.Fprintf(bufConn, "> ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSorted(conn bufferedConn) {
|
func handleSorted(conn bufferedConn) {
|
||||||
// at this piont we've already at least one byte via Peek()
|
// Wish List for protocol detection
|
||||||
|
// * PROXY protocol (and loop)
|
||||||
|
// * tls (and loop) https://github.com/polvi/sni
|
||||||
|
// * http/ws
|
||||||
|
// * irc
|
||||||
|
// * fallback to telnet
|
||||||
|
|
||||||
|
// At this piont we've already at least one byte via Peek()
|
||||||
// so the first packet is available in the buffer
|
// so the first packet is available in the buffer
|
||||||
|
|
||||||
|
// Note: Realistically no tls/http/irc client is going to send so few bytes
|
||||||
|
// (and no router is going to chunk so small)
|
||||||
|
// that it cannot reasonably detect the protocol in the first packet
|
||||||
n := conn.Buffered()
|
n := conn.Buffered()
|
||||||
firstMsg, err := conn.Peek(n)
|
firstMsg, err := conn.Peek(n)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
|
@ -218,7 +304,7 @@ func handleSorted(conn bufferedConn) {
|
||||||
// fmt.Fprintf(os.Stdout, "Weird")
|
// fmt.Fprintf(os.Stdout, "Weird")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
//myChans["general"] <- myMsg{
|
//myRooms["general"] <- myMsg{
|
||||||
myMsgs <- myMsg{
|
myMsgs <- myMsg{
|
||||||
receivedAt: time.Now(),
|
receivedAt: time.Now(),
|
||||||
sender: conn,
|
sender: conn,
|
||||||
|
@ -228,9 +314,9 @@ func handleSorted(conn bufferedConn) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO https://github.com/polvi/sni
|
|
||||||
func handleConnection(netConn net.Conn) {
|
func handleConnection(netConn net.Conn) {
|
||||||
fmt.Fprintf(os.Stdout, "Accepting socket\n")
|
ts := time.Now()
|
||||||
|
fmt.Fprintf(os.Stdout, "[New Connection] (%s) welcome %s\n", ts, netConn.RemoteAddr().String())
|
||||||
|
|
||||||
m := sync.Mutex{}
|
m := sync.Mutex{}
|
||||||
virgin := true
|
virgin := true
|
||||||
|
@ -241,14 +327,15 @@ func handleConnection(netConn net.Conn) {
|
||||||
// But this does
|
// But this does
|
||||||
|
|
||||||
bufConn := newBufferedConn(netConn)
|
bufConn := newBufferedConn(netConn)
|
||||||
myUnsortedConns[bufConn] = true
|
//myUnsortedConns[bufConn] = true
|
||||||
go func() {
|
go func() {
|
||||||
// Handle First Packet
|
// Handle First Packet
|
||||||
fmsg, err := bufConn.Peek(1)
|
_, err := bufConn.Peek(1)
|
||||||
|
//fmsg, err := bufConn.Peek(1)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(os.Stdout, "[First Byte] %s\n", fmsg)
|
//fmt.Fprintf(os.Stdout, "[First Byte] %s\n", fmsg)
|
||||||
|
|
||||||
m.Lock();
|
m.Lock();
|
||||||
if virgin {
|
if virgin {
|
||||||
|
@ -269,7 +356,7 @@ func handleConnection(netConn net.Conn) {
|
||||||
virgin = false
|
virgin = false
|
||||||
// don't block for this
|
// don't block for this
|
||||||
// let it be handled after the unlock
|
// let it be handled after the unlock
|
||||||
defer fmt.Fprintf(netConn, "Welcome to Sample Chat! You appear to be using Telnet.\nYou must authenticate via email to participate\nEmail: ")
|
defer fmt.Fprintf(netConn, "\n\nWelcome to Sample Chat! You appear to be using Telnet.\nYou must authenticate via email to participate\n\nEmail: ")
|
||||||
}
|
}
|
||||||
m.Unlock()
|
m.Unlock()
|
||||||
}
|
}
|
||||||
|
@ -310,11 +397,18 @@ func sendAuthCode(cnf ConfMailer, to string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
// Security XXX
|
||||||
|
// we trust mailgun implicitly and this is just a demo
|
||||||
|
// hence no DoS check on body size for now
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
fmt.Fprintf(os.Stdout, "Here's what Mailgun had to say about the event: %s\n", body)
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 || "{" != string(body[0]) {
|
||||||
|
fmt.Fprintf(os.Stdout, "[Mailgun] Uh-oh...\n[Maigun] Baby Brent says: %s\n", body)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(os.Stdout, "[Mailgun] Status: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
return code, nil
|
return code, nil
|
||||||
}
|
}
|
||||||
|
@ -337,17 +431,18 @@ func main() {
|
||||||
config = Conf{}
|
config = Conf{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
myRawConns := make(map[bufferedConn]bool)
|
||||||
firstMsgs = make(chan myMsg, 128)
|
firstMsgs = make(chan myMsg, 128)
|
||||||
myChans = make(map[string](chan myMsg))
|
//myRooms = make(map[string](chan myMsg))
|
||||||
newConns = make(chan net.Conn, 128)
|
newConns = make(chan net.Conn, 128)
|
||||||
|
authTcpChat = make(chan tcpUser, 128)
|
||||||
newTcpChat = make(chan bufferedConn, 128)
|
newTcpChat = make(chan bufferedConn, 128)
|
||||||
newHttpChat = make(chan bufferedConn, 128)
|
newHttpChat = make(chan bufferedConn, 128)
|
||||||
myRawConns = make(map[net.Conn]bool)
|
//myUnsortedConns = make(map[net.Conn]bool)
|
||||||
myUnsortedConns = make(map[net.Conn]bool)
|
|
||||||
|
|
||||||
// TODO dynamically select on channels?
|
// TODO dynamically select on channels?
|
||||||
// https://stackoverflow.com/questions/19992334/how-to-listen-to-n-channels-dynamic-select-statement
|
// https://stackoverflow.com/questions/19992334/how-to-listen-to-n-channels-dynamic-select-statement
|
||||||
//myChans["general"] = make(chan myMsg, 128)
|
//myRooms["general"] = make(chan myMsg, 128)
|
||||||
myMsgs = make(chan myMsg, 128)
|
myMsgs = make(chan myMsg, 128)
|
||||||
|
|
||||||
var addr string
|
var addr string
|
||||||
|
@ -377,40 +472,77 @@ func main() {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Main event loop handling most access to shared data
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case conn := <- newConns:
|
case conn := <- newConns:
|
||||||
ts := time.Now()
|
|
||||||
fmt.Fprintf(os.Stdout, "[Handle New Connection] [Timestamp] %s\n", ts)
|
|
||||||
// This is short lived
|
// This is short lived
|
||||||
go handleConnection(conn)
|
go handleConnection(conn)
|
||||||
|
case u := <- authTcpChat:
|
||||||
|
// allow to receive messages
|
||||||
|
// (and be counted among the users)
|
||||||
|
myRawConns[u.bufConn] = true
|
||||||
|
// is chan chan the right way to handle this?
|
||||||
|
u.userCount <- len(myRawConns)
|
||||||
|
myMsgs <- myMsg{
|
||||||
|
sender: nil,
|
||||||
|
// TODO fmt.Fprintf()? template?
|
||||||
|
bytes: []byte("<" + u.email + "> joined #general\n"),
|
||||||
|
receivedAt: time.Now(),
|
||||||
|
channel: "general",
|
||||||
|
email: "system",
|
||||||
|
}
|
||||||
case bufConn := <- newTcpChat:
|
case bufConn := <- newTcpChat:
|
||||||
myRawConns[bufConn] = true
|
|
||||||
go handleRaw(bufConn)
|
go handleRaw(bufConn)
|
||||||
case bufConn := <- delTcpChat:
|
case bufConn := <- delTcpChat:
|
||||||
bufConn.Close();
|
// we can safely ignore this error
|
||||||
|
bufConn.Close()
|
||||||
delete(myRawConns, bufConn)
|
delete(myRawConns, bufConn)
|
||||||
case bufConn := <- newHttpChat:
|
case bufConn := <- newHttpChat:
|
||||||
go handleSorted(bufConn)
|
go handleSorted(bufConn)
|
||||||
//case msg := <- myChans["general"]:
|
//case msg := <- myRooms["general"]:
|
||||||
//delete(myChans["general"], bufConn)
|
//delete(myRooms["general"], bufConn)
|
||||||
case msg := <- myMsgs:
|
case msg := <- myMsgs:
|
||||||
ts, err := msg.receivedAt.MarshalJSON()
|
t := msg.receivedAt
|
||||||
if nil != err {
|
tf := "%d-%02d-%02d %02d:%02d:%02d (%s)"
|
||||||
fmt.Fprintf(os.Stderr, "[Error] %s\n", err)
|
var sender string
|
||||||
|
if nil != msg.sender {
|
||||||
|
sender = msg.sender.RemoteAddr().String()
|
||||||
|
} else {
|
||||||
|
sender = "system"
|
||||||
}
|
}
|
||||||
fmt.Fprintf(os.Stdout, "[Timestamp] %s\n", ts)
|
// I wonder if we could use IP detection to get the client's tz
|
||||||
fmt.Fprintf(os.Stdout, "[Remote] %s\n", msg.sender.RemoteAddr().String())
|
// ... could probably make time for this in the authentication loop
|
||||||
fmt.Fprintf(os.Stdout, "[Message] %s\n", msg.bytes);
|
zone, _ := msg.receivedAt.Zone()
|
||||||
|
|
||||||
|
//ts, err := msg.receivedAt.MarshalJSON()
|
||||||
|
fmt.Fprintf(os.Stdout, tf + " [%s] (%s):\n\t%s",
|
||||||
|
t.Year(), t.Month(), t.Day(),
|
||||||
|
t.Hour(), t.Minute(), t.Second(), zone,
|
||||||
|
sender,
|
||||||
|
msg.email, msg.bytes)
|
||||||
|
|
||||||
for conn, _ := range myRawConns {
|
for conn, _ := range myRawConns {
|
||||||
|
// Don't echo back to the original client
|
||||||
if msg.sender == conn {
|
if msg.sender == conn {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// backlogged connections could prevent a next write,
|
|
||||||
// so this should be refactored into a goroutine
|
// Don't block the rest of the loop
|
||||||
// And what to do about slow clients that get behind (or DoS)?
|
// TODO maybe use a chan to send to the socket's event loop
|
||||||
// SetDeadTime and Disconnect them?
|
go func() {
|
||||||
conn.Write(msg.bytes)
|
// Protect against malicious clients to prevent DoS
|
||||||
|
// https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/
|
||||||
|
timeoutDuration := 5 * time.Second
|
||||||
|
conn.SetWriteDeadline(time.Now().Add(timeoutDuration))
|
||||||
|
_, err := fmt.Fprintf(conn, tf + " [%s]: %s",
|
||||||
|
t.Year(), t.Month(), t.Day(),
|
||||||
|
t.Hour(), t.Minute(), t.Second(), zone,
|
||||||
|
msg.email, msg.bytes)
|
||||||
|
if nil != err {
|
||||||
|
delTcpChat <- conn
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
case msg := <- firstMsgs:
|
case msg := <- firstMsgs:
|
||||||
fmt.Fprintf(os.Stdout, "f [First Message]\n")
|
fmt.Fprintf(os.Stdout, "f [First Message]\n")
|
||||||
|
|
Loading…
Reference in New Issue