mostly just cleanup I think...
This commit is contained in:
parent
34ee3924d3
commit
64425b41d2
218
chatserver.go
218
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
|
||||
// https://stackoverflow.com/questions/51472020/how-to-get-the-size-of-available-tcp-data
|
||||
type bufferedConn struct {
|
||||
|
@ -70,15 +76,16 @@ type myMsg struct {
|
|||
bytes []byte
|
||||
receivedAt time.Time
|
||||
channel string
|
||||
email string
|
||||
}
|
||||
|
||||
var firstMsgs chan myMsg
|
||||
var myChans map[string](chan myMsg)
|
||||
//var myRooms map[string](chan myMsg)
|
||||
var myMsgs chan myMsg
|
||||
var myUnsortedConns map[net.Conn]bool
|
||||
var myRawConns map[net.Conn]bool
|
||||
//var myUnsortedConns map[net.Conn]bool
|
||||
var newConns chan net.Conn
|
||||
var newTcpChat chan bufferedConn
|
||||
var authTcpChat chan tcpUser
|
||||
var delTcpChat chan bufferedConn
|
||||
var newHttpChat chan bufferedConn
|
||||
var delHttpChat chan bufferedConn
|
||||
|
@ -117,7 +124,7 @@ func handleRaw(bufConn bufferedConn) {
|
|||
// Handle all subsequent packets
|
||||
buffer := make([]byte, 1024)
|
||||
for {
|
||||
fmt.Fprintf(os.Stdout, "[raw] Waiting for message...\n");
|
||||
//fmt.Fprintf(os.Stdout, "[raw] Waiting for message...\n");
|
||||
count, err := bufConn.Read(buffer)
|
||||
if nil != err {
|
||||
if io.EOF != err {
|
||||
|
@ -125,7 +132,6 @@ func handleRaw(bufConn bufferedConn) {
|
|||
}
|
||||
fmt.Fprintf(os.Stdout, "Ending socket\n")
|
||||
|
||||
// TODO put this in a channel to prevent data races
|
||||
delTcpChat <- bufConn
|
||||
break
|
||||
}
|
||||
|
@ -140,7 +146,9 @@ func handleRaw(bufConn bufferedConn) {
|
|||
|
||||
if !authn {
|
||||
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
|
||||
email = strings.TrimSpace(string(buf[:count]))
|
||||
emailParts := strings.Split(email, "@")
|
||||
|
@ -148,12 +156,53 @@ func handleRaw(bufConn bufferedConn) {
|
|||
fmt.Fprintf(bufConn, "Email: ")
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "email: '%v'\n", []byte(email))
|
||||
code, err = sendAuthCode(config.Mailer, strings.TrimSpace(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))
|
||||
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== 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: ")
|
||||
continue
|
||||
}
|
||||
|
@ -162,27 +211,64 @@ func handleRaw(bufConn bufferedConn) {
|
|||
fmt.Fprintf(bufConn, "Incorrect Code\nAuth Code: ")
|
||||
} else {
|
||||
authn = true
|
||||
fmt.Fprintf(bufConn, "Welcome to #general! (TODO `/help' for list of commands)\n")
|
||||
// TODO number of users
|
||||
//fmt.Fprintf(bufConn, "Welcome to #general! TODO `/list' to see channels. `/join chname' to switch.\n")
|
||||
time.Sleep(150 * 1000000)
|
||||
fmt.Fprintf(bufConn, "\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
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stdout, "Queing message...\n");
|
||||
//myChans["general"] <- myMsg{
|
||||
//fmt.Fprintf(os.Stdout, "Queing message...\n");
|
||||
//myRooms["general"] <- myMsg{
|
||||
myMsgs <- myMsg{
|
||||
receivedAt: time.Now(),
|
||||
sender: bufConn,
|
||||
bytes: buf[0:count],
|
||||
channel: "general",
|
||||
email: email,
|
||||
}
|
||||
//fmt.Fprintf(bufConn, "> ")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// 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()
|
||||
firstMsg, err := conn.Peek(n)
|
||||
if nil != err {
|
||||
|
@ -218,7 +304,7 @@ func handleSorted(conn bufferedConn) {
|
|||
// fmt.Fprintf(os.Stdout, "Weird")
|
||||
continue
|
||||
}
|
||||
//myChans["general"] <- myMsg{
|
||||
//myRooms["general"] <- myMsg{
|
||||
myMsgs <- myMsg{
|
||||
receivedAt: time.Now(),
|
||||
sender: conn,
|
||||
|
@ -228,9 +314,9 @@ func handleSorted(conn bufferedConn) {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO https://github.com/polvi/sni
|
||||
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{}
|
||||
virgin := true
|
||||
|
@ -241,14 +327,15 @@ func handleConnection(netConn net.Conn) {
|
|||
// But this does
|
||||
|
||||
bufConn := newBufferedConn(netConn)
|
||||
myUnsortedConns[bufConn] = true
|
||||
//myUnsortedConns[bufConn] = true
|
||||
go func() {
|
||||
// Handle First Packet
|
||||
fmsg, err := bufConn.Peek(1)
|
||||
_, err := bufConn.Peek(1)
|
||||
//fmsg, err := bufConn.Peek(1)
|
||||
if nil != err {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "[First Byte] %s\n", fmsg)
|
||||
//fmt.Fprintf(os.Stdout, "[First Byte] %s\n", fmsg)
|
||||
|
||||
m.Lock();
|
||||
if virgin {
|
||||
|
@ -269,7 +356,7 @@ func handleConnection(netConn net.Conn) {
|
|||
virgin = false
|
||||
// don't block for this
|
||||
// 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()
|
||||
}
|
||||
|
@ -310,11 +397,18 @@ func sendAuthCode(cnf ConfMailer, to string) (string, error) {
|
|||
}
|
||||
|
||||
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)
|
||||
if nil != 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
|
||||
}
|
||||
|
@ -337,17 +431,18 @@ func main() {
|
|||
config = Conf{}
|
||||
}
|
||||
|
||||
myRawConns := make(map[bufferedConn]bool)
|
||||
firstMsgs = make(chan myMsg, 128)
|
||||
myChans = make(map[string](chan myMsg))
|
||||
//myRooms = make(map[string](chan myMsg))
|
||||
newConns = make(chan net.Conn, 128)
|
||||
authTcpChat = make(chan tcpUser, 128)
|
||||
newTcpChat = 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?
|
||||
// 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)
|
||||
|
||||
var addr string
|
||||
|
@ -377,40 +472,77 @@ func main() {
|
|||
}
|
||||
}()
|
||||
|
||||
// Main event loop handling most access to shared data
|
||||
for {
|
||||
select {
|
||||
case conn := <- newConns:
|
||||
ts := time.Now()
|
||||
fmt.Fprintf(os.Stdout, "[Handle New Connection] [Timestamp] %s\n", ts)
|
||||
// This is short lived
|
||||
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:
|
||||
myRawConns[bufConn] = true
|
||||
go handleRaw(bufConn)
|
||||
case bufConn := <- delTcpChat:
|
||||
bufConn.Close();
|
||||
// we can safely ignore this error
|
||||
bufConn.Close()
|
||||
delete(myRawConns, bufConn)
|
||||
case bufConn := <- newHttpChat:
|
||||
go handleSorted(bufConn)
|
||||
//case msg := <- myChans["general"]:
|
||||
//delete(myChans["general"], bufConn)
|
||||
//case msg := <- myRooms["general"]:
|
||||
//delete(myRooms["general"], bufConn)
|
||||
case msg := <- myMsgs:
|
||||
ts, err := msg.receivedAt.MarshalJSON()
|
||||
if nil != err {
|
||||
fmt.Fprintf(os.Stderr, "[Error] %s\n", err)
|
||||
t := msg.receivedAt
|
||||
tf := "%d-%02d-%02d %02d:%02d:%02d (%s)"
|
||||
var sender string
|
||||
if nil != msg.sender {
|
||||
sender = msg.sender.RemoteAddr().String()
|
||||
} else {
|
||||
sender = "system"
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "[Timestamp] %s\n", ts)
|
||||
fmt.Fprintf(os.Stdout, "[Remote] %s\n", msg.sender.RemoteAddr().String())
|
||||
fmt.Fprintf(os.Stdout, "[Message] %s\n", msg.bytes);
|
||||
// I wonder if we could use IP detection to get the client's tz
|
||||
// ... could probably make time for this in the authentication loop
|
||||
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 {
|
||||
// Don't echo back to the original client
|
||||
if msg.sender == conn {
|
||||
continue
|
||||
}
|
||||
// backlogged connections could prevent a next write,
|
||||
// so this should be refactored into a goroutine
|
||||
// And what to do about slow clients that get behind (or DoS)?
|
||||
// SetDeadTime and Disconnect them?
|
||||
conn.Write(msg.bytes)
|
||||
|
||||
// Don't block the rest of the loop
|
||||
// TODO maybe use a chan to send to the socket's event loop
|
||||
go func() {
|
||||
// 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:
|
||||
fmt.Fprintf(os.Stdout, "f [First Message]\n")
|
||||
|
|
Loading…
Reference in New Issue