even more cleanup
This commit is contained in:
parent
92340069b1
commit
44d605d38a
56
README.md
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
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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, "> ")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ 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 {
|
||||||
|
Addr string `yaml:"addr,omitempty"`
|
||||||
Port uint `yaml:"port,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,3 +1,4 @@
|
||||||
|
addr: 127.0.0.1
|
||||||
port: 4080
|
port: 4080
|
||||||
root_path: ./public
|
root_path: ./public
|
||||||
mailer:
|
mailer:
|
||||||
|
|
|
@ -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…
Reference in New Issue