Compare commits
	
		
			2 Commits
		
	
	
		
			0f7580954e
			...
			e7ae08c8fe
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e7ae08c8fe | |||
| a26cfccd19 | 
							
								
								
									
										228
									
								
								chatserver-http.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								chatserver-http.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										200
									
								
								chatserver-telnet.go
									
									
									
									
									
										Normal 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 | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										417
									
								
								chatserver.go
									
									
									
									
									
								
							
							
						
						
									
										417
									
								
								chatserver.go
									
									
									
									
									
								
							| @ -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 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user