Browse Source

even more cleanup

master
AJ ONeal 6 years ago
parent
commit
44d605d38a
  1. 56
      README.md
  2. 15
      chatserver-http.go
  3. 7
      chatserver-telnet.go
  4. 49
      chatserver.go
  5. 1
      config.sample.yml
  6. 12
      public/index.html

56
README.md

@ -32,23 +32,71 @@ You can connect multiple clients.
telnet localhost 4080 telnet localhost 4080
``` ```
You can also use HTTP. The API docs and examples can be seen at <http://localhost:4080> You can also use HTTP.
``` ```
curl http://localhost:4080 curl http://localhost:4080
``` ```
# API Docs
The API docs and examples can be seen at <http://localhost:4080>
# Project Approach
I've understood theoretical principles of Go for a long time and I've always loved it.
However, most of that came from watching Go Tech Talks and going to meetups.
This is my first Go project and my primary goal was to learn how to use Go
for the kinds of things that I'm personally interested in while also satisfying
a mix of the requirements and optional add-ons, and show you that I know how
to write code and learn.
Criteria Met
-----
* [x] Works
* [x] Attention to Detail
* [x] Security
* [x] UX
* [x] Performance
* [x] Commenting
* [x] Creativity
Limitations
----------
* [ ] Good coding style
This is my first Go project so I was learning as I went and trying different approaches.
As a result my code style is inconsistent and probably does some things the wrong way
(especially confusing since I probably did it the right way in other places).
Implemented Implemented
----- -----
* [x] Awesome telnet server (would * [x] Awesome telnet server
* [x] HTTP API (no UI for the sake of time) * [x] Multiple clients can connect
* [x] Messages are relayed to all clients
* [x] Includes timestamp, name of client
* [x] Config file for port
* [x] HTTP API
* [x] Post message
* [x] List messages
* [x] Serve Docs
* [x] Working curl examples
* [x] Multiplex the same port (because I wanted to learn) * [x] Multiplex the same port (because I wanted to learn)
* [x] E-mail "magic link" authentication (minus the link since it's localhost) * [x] E-mail "magic link" authentication (minus the link since it's localhost)
Not Implemented Not Implemented
---- ----
* [ ] Write to log file (just `go run ./chatserver.go > /path/to/log` I don't think these things would be difficult to add,
but I was having fun learning lots of other things
and I figured Some of these things I didn't implement
* [ ] local log file
* [ ] Listening IP config
* [ ] Rooms * [ ] Rooms
* [ ] Blocking
* [ ] UI for HTTP API

15
chatserver-http.go

@ -3,6 +3,7 @@ package main
import ( import (
"crypto/subtle" "crypto/subtle"
"fmt" "fmt"
"net"
"net/http" "net/http"
"os" "os"
"path" "path"
@ -12,6 +13,20 @@ import (
restful "github.com/emicklei/go-restful" restful "github.com/emicklei/go-restful"
) )
type myHttpServer struct {
chans chan bufferedConn
net.Listener
}
func (m *myHttpServer) Accept() (net.Conn, error) {
bufConn := <-m.chans
return bufConn, nil
}
func newHttpServer(l net.Listener) *myHttpServer {
return &myHttpServer{make(chan bufferedConn), l}
}
// TODO I probably should just make the non-exportable properties private/lowercase // TODO I probably should just make the non-exportable properties private/lowercase
type authReq struct { type authReq struct {
Cid string `json:"cid"` Cid string `json:"cid"`

7
chatserver-telnet.go

@ -34,7 +34,6 @@ func handleTelnetConn(bufConn bufferedConn) {
if io.EOF != err { if io.EOF != err {
fmt.Fprintf(os.Stderr, "Non-EOF socket error: %s\n", err) fmt.Fprintf(os.Stderr, "Non-EOF socket error: %s\n", err)
} }
fmt.Fprintf(os.Stdout, "Ending socket\n")
if nil != u { if nil != u {
cleanTelnet <- *u cleanTelnet <- *u
@ -136,8 +135,7 @@ func handleTelnetConn(bufConn bufferedConn) {
newMsg: make(chan string, 10), // reasonably sized newMsg: make(chan string, 10), // reasonably sized
} }
authTelnet <- *u authTelnet <- *u
// prevent data race on len(myRawConns) // prevent data race on len(telnetConns)
// XXX (there can't be a race between these two lines, right?)
count := <-u.userCount count := <-u.userCount
close(u.userCount) close(u.userCount)
u.userCount = nil u.userCount = nil
@ -168,8 +166,6 @@ func handleTelnetConn(bufConn bufferedConn) {
continue continue
} }
//fmt.Fprintf(os.Stdout, "Queing message...\n")
//myRooms["general"] <- myMsg{
broadcastMsg <- myMsg{ broadcastMsg <- myMsg{
ReceivedAt: time.Now(), ReceivedAt: time.Now(),
sender: bufConn, sender: bufConn,
@ -177,7 +173,6 @@ func handleTelnetConn(bufConn bufferedConn) {
Channel: "general", Channel: "general",
User: email, User: email,
} }
//fmt.Fprintf(bufConn, "> ")
} }
} }

49
chatserver.go

@ -28,7 +28,8 @@ import (
// I'm not sure how to pass nested structs, so I de-nested this. // I'm not sure how to pass nested structs, so I de-nested this.
// TODO Learn if passing nested structs is desirable? // TODO Learn if passing nested structs is desirable?
type Conf struct { type Conf struct {
Port uint `yaml:"port,omitempty"` Addr string `yaml:"addr,omitempty"`
Port uint `yaml:"port,omitempty"`
Mailer ConfMailer Mailer ConfMailer
RootPath string `yaml:"root_path,omitempty"` RootPath string `yaml:"root_path,omitempty"`
} }
@ -100,7 +101,7 @@ var authTelnet chan tcpUser
var cleanTelnet chan tcpUser var cleanTelnet chan tcpUser
var gotClientHello chan bufferedConn var gotClientHello chan bufferedConn
// Http // HTTP
var demuxHttpClient chan bufferedConn var demuxHttpClient chan bufferedConn
var newAuthReqs chan authReq var newAuthReqs chan authReq
var valAuthReqs chan authReq var valAuthReqs chan authReq
@ -277,25 +278,11 @@ func sendAuthCode(cnf ConfMailer, to string) (string, error) {
return code, nil return code, nil
} }
type myHttpServer struct {
chans chan bufferedConn
net.Listener
}
func (m *myHttpServer) Accept() (net.Conn, error) {
bufConn := <-m.chans
return bufConn, nil
}
func newHttpServer(l net.Listener) *myHttpServer {
return &myHttpServer{make(chan bufferedConn), l}
}
var config Conf var config Conf
func main() { func main() {
flag.Usage = usage flag.Usage = usage
port := flag.Uint("telnet-port", 0, "tcp telnet chat port") port := flag.Uint("port", 0, "tcp telnet chat port")
confname := flag.String("conf", "./config.yml", "yaml config file") confname := flag.String("conf", "./config.yml", "yaml config file")
flag.Parse() flag.Parse()
@ -318,7 +305,7 @@ func main() {
virginConns = make(chan net.Conn, 128) virginConns = make(chan net.Conn, 128)
// TCP & Authentication // TCP & Authentication
myRawConns := make(map[bufferedConn]tcpUser) telnetConns := make(map[bufferedConn]tcpUser)
wantsServerHello = make(chan bufferedConn, 128) wantsServerHello = make(chan bufferedConn, 128)
authTelnet = make(chan tcpUser, 128) authTelnet = make(chan tcpUser, 128)
@ -344,9 +331,9 @@ func main() {
var addr string var addr string
if 0 != int(*port) { if 0 != int(*port) {
addr = ":" + strconv.Itoa(int(*port)) addr = config.Addr + ":" + strconv.Itoa(int(*port))
} else { } else {
addr = ":" + strconv.Itoa(int(config.Port)) addr = config.Addr + ":" + strconv.Itoa(int(config.Port))
} }
// https://golang.org/pkg/net/#Conn // https://golang.org/pkg/net/#Conn
@ -388,8 +375,8 @@ func main() {
wsApi.Route(wsApi.GET("/hello").To(serveHello)) wsApi.Route(wsApi.GET("/hello").To(serveHello))
wsApi.Route(wsApi.POST("/sessions").To(requestAuth)) wsApi.Route(wsApi.POST("/sessions").To(requestAuth))
wsApi.Route(wsApi.POST("/sessions/{cid}").To(issueToken)) wsApi.Route(wsApi.POST("/sessions/{cid}").To(issueToken))
wsApi.Route(wsApi.GET("/rooms/general").Filter(requireToken).To(listMsgs)) wsApi.Route(wsApi.GET("/rooms/{room}").Filter(requireToken).To(listMsgs))
wsApi.Route(wsApi.POST("/rooms/general").Filter(requireToken).To(postMsg)) wsApi.Route(wsApi.POST("/rooms/{room}").Filter(requireToken).To(postMsg))
container.Add(wsApi) container.Add(wsApi)
server := &http.Server{ server := &http.Server{
@ -410,9 +397,9 @@ func main() {
case u := <-authTelnet: case u := <-authTelnet:
// allow to receive messages // allow to receive messages
// (and be counted among the users) // (and be counted among the users)
myRawConns[u.bufConn] = u telnetConns[u.bufConn] = u
// is chan chan the right way to handle this? // is chan chan the right way to handle this?
u.userCount <- len(myRawConns) u.userCount <- len(telnetConns)
broadcastMsg <- myMsg{ broadcastMsg <- myMsg{
sender: nil, sender: nil,
// TODO fmt.Fprintf()? template? // TODO fmt.Fprintf()? template?
@ -442,9 +429,19 @@ func main() {
go handleTelnetConn(bufConn) go handleTelnetConn(bufConn)
case u := <-cleanTelnet: case u := <-cleanTelnet:
// we can safely ignore this error, if any // we can safely ignore this error, if any
if "" != u.email {
broadcastMsg <- myMsg{
sender: nil,
// TODO fmt.Fprintf()? template?
Message: "<" + u.email + "> left #general\n",
ReceivedAt: time.Now(),
Channel: "general",
User: "system",
}
}
close(u.newMsg) close(u.newMsg)
u.bufConn.Close() u.bufConn.Close()
delete(myRawConns, u.bufConn) delete(telnetConns, u.bufConn)
case bufConn := <-gotClientHello: case bufConn := <-gotClientHello:
go muxTcp(bufConn) go muxTcp(bufConn)
case bufConn := <-demuxHttpClient: case bufConn := <-demuxHttpClient:
@ -480,7 +477,7 @@ func main() {
sender, sender,
msg.User, msg.Message) msg.User, msg.Message)
for _, u := range myRawConns { for _, u := range telnetConns {
// Don't echo back to the original client // Don't echo back to the original client
if msg.sender == u.bufConn { if msg.sender == u.bufConn {
continue continue

1
config.sample.yml

@ -1,3 +1,4 @@
addr: 127.0.0.1
port: 4080 port: 4080
root_path: ./public root_path: ./public
mailer: mailer:

12
public/index.html

@ -5,22 +5,22 @@
<pre><code># Ask for an auth code (swap sub) <pre><code># Ask for an auth code (swap sub)
curl -X POST http://localhost:4080/api/sessions \ curl -X POST http://localhost:4080/api/sessions \
-H 'Content-Type: application/json; charset=utf-8' \ -H 'Content-Type: application/json; charset=utf-8' \
-d '{"sub":"jon@example.com"}' -d '{"sub":"<strong><em>jon@example.com</em></strong>"}'
# Validate auth code (swap session id, sub, and otp) # Validate auth code (swap session id, sub, and otp)
curl -X POST http://localhost:4080/api/sessions/xyz \ curl -X POST http://localhost:4080/api/sessions/<strong><em>xyz</em></strong> \
-H 'Content-Type: application/json; charset=utf-8' \ -H 'Content-Type: application/json; charset=utf-8' \
-d '{"otp":"secret123"}' -d '{"otp":"<strong><em>secret123</em></strong>"}'
# Post a message (swap api-token) # Post a message (swap api-token)
curl -X POST http://localhost:4080/api/rooms/general \ curl -X POST http://localhost:4080/api/rooms/general \
-H 'Authorization: Bearer api-token' \ -H 'Authorization: Bearer <strong><em>api-token</em></strong>' \
-H 'Content-Type: application/json; charset=utf-8' \ -H 'Content-Type: application/json; charset=utf-8' \
-d '{"message":"hello"}' -d '{"message":"Hello, World!"}'
# Get a room's messages (swap api-token, since unix-epoch) # Get a room's messages (swap api-token, since unix-epoch)
curl http://localhost:4080/api/rooms/general?since=0 \ curl http://localhost:4080/api/rooms/general?since=0 \
-H 'Authorization: Bearer api-token' -H 'Authorization: Bearer <strong><em>api-token</em></strong>'
</code></pre> </code></pre>
</body> </body>
</html> </html>

Loading…
Cancel
Save