some file organization
This commit is contained in:
		
							parent
							
								
									a26cfccd19
								
							
						
					
					
						commit
						e7ae08c8fe
					
				
							
								
								
									
										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 | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										414
									
								
								chatserver.go
									
									
									
									
									
								
							
							
						
						
									
										414
									
								
								chatserver.go
									
									
									
									
									
								
							| @ -7,7 +7,6 @@ import ( | |||||||
| 	"bufio" | 	"bufio" | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"crypto/rand" | 	"crypto/rand" | ||||||
| 	"crypto/subtle" |  | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"flag" | 	"flag" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| @ -17,7 +16,6 @@ import ( | |||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path" |  | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
| @ -96,13 +94,14 @@ type chatHist struct { | |||||||
| var myChatHist chatHist | var myChatHist chatHist | ||||||
| var broadcastMsg chan myMsg | var broadcastMsg chan myMsg | ||||||
| 
 | 
 | ||||||
| var newConns chan net.Conn | var virginConns chan net.Conn | ||||||
| var wantsServerHello chan bufferedConn | var wantsServerHello chan bufferedConn | ||||||
| var authTcpChat chan tcpUser | var authTcpChat chan tcpUser | ||||||
| var delTcpChat chan tcpUser | var delTcpChat chan tcpUser | ||||||
| var gotClientHello chan bufferedConn | var gotClientHello chan bufferedConn | ||||||
|  | 
 | ||||||
|  | // Http | ||||||
| var demuxHttpClient chan bufferedConn | var demuxHttpClient chan bufferedConn | ||||||
| var delHttpChat chan bufferedConn |  | ||||||
| var newAuthReqs chan authReq | var newAuthReqs chan authReq | ||||||
| var valAuthReqs chan authReq | var valAuthReqs chan authReq | ||||||
| var delAuthReqs chan authReq | var delAuthReqs chan authReq | ||||||
| @ -127,193 +126,6 @@ func genAuthCode() (string, error) { | |||||||
| 	return base64.URLEncoding.EncodeToString(b), nil | 	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) { | func muxTcp(conn bufferedConn) { | ||||||
| 	// Wish List for protocol detection | 	// Wish List for protocol detection | ||||||
| 	// * PROXY protocol (and loop) | 	// * PROXY protocol (and loop) | ||||||
| @ -483,219 +295,6 @@ func newHttpServer(l net.Listener) *myHttpServer { | |||||||
| 
 | 
 | ||||||
| var config Conf | 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() { | func main() { | ||||||
| 	flag.Usage = usage | 	flag.Usage = usage | ||||||
| 	port := flag.Uint("telnet-port", 0, "tcp telnet chat port") | 	port := flag.Uint("telnet-port", 0, "tcp telnet chat port") | ||||||
| @ -718,7 +317,7 @@ func main() { | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// The magical sorting hat | 	// The magical sorting hat | ||||||
| 	newConns = make(chan net.Conn, 128) | 	virginConns = make(chan net.Conn, 128) | ||||||
| 
 | 
 | ||||||
| 	// TCP & Authentication | 	// TCP & Authentication | ||||||
| 	myRawConns := make(map[bufferedConn]tcpUser) | 	myRawConns := make(map[bufferedConn]tcpUser) | ||||||
| @ -768,7 +367,7 @@ func main() { | |||||||
| 				// Could a connection abort or end before it's handled? | 				// Could a connection abort or end before it's handled? | ||||||
| 				fmt.Fprintf(os.Stderr, "Error accepting connection:\n%s\n", err) | 				fmt.Fprintf(os.Stderr, "Error accepting connection:\n%s\n", err) | ||||||
| 			} | 			} | ||||||
| 			newConns <- conn | 			virginConns <- conn | ||||||
| 		} | 		} | ||||||
| 	}() | 	}() | ||||||
| 
 | 
 | ||||||
| @ -807,7 +406,7 @@ func main() { | |||||||
| 	// Main event loop handling most access to shared data | 	// Main event loop handling most access to shared data | ||||||
| 	for { | 	for { | ||||||
| 		select { | 		select { | ||||||
| 		case conn := <-newConns: | 		case conn := <-virginConns: | ||||||
| 			// This is short lived | 			// This is short lived | ||||||
| 			go handleConnection(conn) | 			go handleConnection(conn) | ||||||
| 		case u := <-authTcpChat: | 		case u := <-authTcpChat: | ||||||
| @ -842,6 +441,7 @@ func main() { | |||||||
| 			go handleTelnetConn(bufConn) | 			go handleTelnetConn(bufConn) | ||||||
| 		case u := <-delTcpChat: | 		case u := <-delTcpChat: | ||||||
| 			// we can safely ignore this error, if any | 			// we can safely ignore this error, if any | ||||||
|  | 			close(u.newMsg) | ||||||
| 			u.bufConn.Close() | 			u.bufConn.Close() | ||||||
| 			delete(myRawConns, u.bufConn) | 			delete(myRawConns, u.bufConn) | ||||||
| 		case bufConn := <-gotClientHello: | 		case bufConn := <-gotClientHello: | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user