diff --git a/cmd/vail/book.go b/cmd/vail/book.go index 15c0823..95b9606 100644 --- a/cmd/vail/book.go +++ b/cmd/vail/book.go @@ -1,19 +1,23 @@ package main import ( - "io" "log" ) +// Book maps names to repeaters +// +// It ensures that names map 1-1 to repeaters. type Book struct { - entries map[string]*Repeater - events chan bookEvent + entries map[string]*Repeater + events chan bookEvent + makeRepeater func() *Repeater } func NewBook() Book { return Book{ - entries: make(map[string]*Repeater), - events: make(chan bookEvent, 5), + entries: make(map[string]*Repeater), + events: make(chan bookEvent, 5), + makeRepeater: NewRepeater, } } @@ -28,34 +32,38 @@ const ( type bookEvent struct { eventType bookEventType name string - w io.Writer - p []byte + sender MessageSender + m Message } -func (b Book) Join(name string, w io.Writer) { +// Join adds a writer to a named repeater +func (b Book) Join(name string, sender MessageSender) { b.events <- bookEvent{ eventType: joinEvent, name: name, - w: w, + sender: sender, } } -func (b Book) Part(name string, w io.Writer) { +// Part removes a writer from a named repeater +func (b Book) Part(name string, sender MessageSender) { b.events <- bookEvent{ eventType: partEvent, name: name, - w: w, + sender: sender, } } -func (b Book) Send(name string, p []byte) { +// Send transmits a message to the named repeater +func (b Book) Send(name string, m Message) { b.events <- bookEvent{ eventType: sendEvent, name: name, - p: p, + m: m, } } +// Run is the endless run loop func (b Book) Run() { for { b.loop() @@ -69,16 +77,16 @@ func (b Book) loop() { switch event.eventType { case joinEvent: if !ok { - repeater = NewRepeater() + repeater = b.makeRepeater() b.entries[event.name] = repeater } - repeater.Join(event.w) + repeater.Join(event.sender) case partEvent: if !ok { log.Println("WARN: Parting an empty channel:", event.name) break } - repeater.Part(event.w) + repeater.Part(event.sender) if repeater.Listeners() == 0 { delete(b.entries, event.name) } @@ -87,6 +95,6 @@ func (b Book) loop() { log.Println("WARN: Sending to an empty channel:", event.name) break } - repeater.Send(event.p) + repeater.Send(event.m) } } diff --git a/cmd/vail/book_test.go b/cmd/vail/book_test.go index 0eabb9f..1026832 100644 --- a/cmd/vail/book_test.go +++ b/cmd/vail/book_test.go @@ -1,59 +1,57 @@ package main import ( - "bytes" "testing" ) func TestBook(t *testing.T) { - b := NewBook() + b := Book{ + entries: make(map[string]*Repeater), + events: make(chan bookEvent, 5), + makeRepeater: NewTestingRepeater, + } - buf1 := bytes.NewBufferString("buf1") - b.Join("moo", buf1) + c1 := NewTestingClient(t) + b.Join("moo", c1) b.loop() if len(b.entries) != 1 { t.Error("Wrong number of entries") } + c1.Expect(1) // Send to an empty channel - b.Send("merf", []byte("goober")) + m := Message{0, 0, []uint16{22, 33}} + b.Send("merf", m) b.loop() - if buf1.String() != "buf1" { + if c1.Len() > 0 { t.Error("Sending to empty channel sent to non-empty channel") } // Send to a non-empty channel! - b.Send("moo", []byte("goober")) + b.Send("moo", m) b.loop() - if buf1.String() != "buf1goober" { - t.Error("Sending didn't work") - } + c1.Expect(1, 22, 33) // Join another client - buf2 := bytes.NewBufferString("buf2") - b.Join("moo", buf2) + c2 := NewTestingClient(t) + b.Join("moo", c2) b.loop() + c1.Expect(2) + c2.Expect(2) // Send to both - b.Send("moo", []byte("snerk")) + m.Duration = append(m.Duration, 44) + b.Send("moo", m) b.loop() - if buf1.String() != "buf1goobersnerk" { - t.Error("Send to 2-member channel busted", buf1) - } - if buf2.String() != "buf2snerk" { - t.Error("Send to 2-member channel busted", buf2) - } + c1.Expect(2, 22, 33, 44) + c2.Expect(2, 22, 33, 44) // Part a client - b.Part("moo", buf1) + b.Part("moo", c1) b.loop() + c2.Expect(1) - b.Send("moo", []byte("peanut")) + b.Send("moo", m) b.loop() - if buf1.String() != "buf1goobersnerk" { - t.Error("Parted channel but still getting messages", buf1) - } - if buf2.String() != "buf2snerkpeanut" { - t.Error("Someone else parting somehow messed up sends", buf2) - } + c2.Expect(1, 22, 33, 44) } diff --git a/cmd/vail/main.go b/cmd/vail/main.go index b602fa7..3d66079 100644 --- a/cmd/vail/main.go +++ b/cmd/vail/main.go @@ -1,49 +1,145 @@ package main import ( + "context" + "encoding/json" "fmt" - "time" - "os" "log" "net/http" - "golang.org/x/net/websocket" + "os" + "time" + + "nhooyr.io/websocket" ) var book Book -type Client struct { - repeaterName string +const JsonProtocol = "json.vail.woozle.org" +const BinaryProtocol = "binary.vail.woozle.org" + +// Clock defines an interface for getting the current time. +// +// We use this in testing to provide a fixed value for the current time, so we +// can still compare clocks. +type Clock interface { + Now() time.Time } -func (c Client) Handle(ws *websocket.Conn) { - ws.MaxPayloadBytes = 500 - book.Join(c.repeaterName, ws) - defer book.Part(c.repeaterName, ws) - - // Tell the client what time we think it is - fmt.Fprintf(ws, "[%d]", time.Now().UnixNano() / time.Millisecond.Nanoseconds()) +// WallClock is a Clock which provides the actual time +type WallClock struct{} - for { - buf := make([]byte, ws.MaxPayloadBytes) +func (WallClock) Now() time.Time { + return time.Now() +} - if n, err := ws.Read(buf); err != nil { - break - } else { - buf = buf[:n] - } - - book.Send(c.repeaterName, buf) +// VailWebSocketConnection reads and writes Message structs +type VailWebSocketConnection struct { + *websocket.Conn + usingJSON bool +} + +func (c *VailWebSocketConnection) Receive() (Message, error) { + var m Message + messageType, buf, err := c.Read(context.Background()) + if err != nil { + return m, err } + + if messageType == websocket.MessageText { + err = json.Unmarshal(buf, &m) + } else { + err = m.UnmarshalBinary(buf) + } + return m, err +} + +func (c *VailWebSocketConnection) Send(m Message) error { + var err error + var buf []byte + var messageType websocket.MessageType + + if c.usingJSON { + messageType = websocket.MessageText + buf, err = json.Marshal(m) + } else { + messageType = websocket.MessageBinary + buf, err = m.MarshalBinary() + } + if err != nil { + return err + } + + return c.Write(context.Background(), messageType, buf) } func ChatHandler(w http.ResponseWriter, r *http.Request) { - c := Client { - repeaterName: r.FormValue("repeater"), + forwardedFor := r.Header.Get("X-Forwarded-For") + client := fmt.Sprintf("<%s|%s>", forwardedFor, r.RemoteAddr) + + // Set up websocket + ws, err := websocket.Accept( + w, r, + &websocket.AcceptOptions{ + Subprotocols: []string{JsonProtocol, BinaryProtocol}, + }, + ) + if err != nil { + log.Println(err) + return + } + defer ws.Close(websocket.StatusInternalError, "Internal error") + + // Create our Vail websocket connection for books to send to + sock := VailWebSocketConnection{ + Conn: ws, } - // This API is confusing as hell. - // I suspect there's a better way to do this. - websocket.Handler(c.Handle).ServeHTTP(w, r) + // websockets apparently sends a subprotocol string, so we can ignore Accept headers! + switch ws.Subprotocol() { + case JsonProtocol: + sock.usingJSON = true + case BinaryProtocol: + sock.usingJSON = false + default: + ws.Close(websocket.StatusPolicyViolation, "client must speak a vail protocol") + return + } + + // Join the repeater + repeaterName := r.FormValue("repeater") + book.Join(repeaterName, &sock) + defer book.Part(repeaterName, &sock) + + log.Println(client, repeaterName, "connect") + + for { + // Read a packet + m, err := sock.Receive() + if err != nil { + ws.Close(websocket.StatusInvalidFramePayloadData, err.Error()) + break + } + + // If it's empty, skip it + if len(m.Duration) == 0 { + continue + } + + // If it's wildly out of time, reject it + timeDelta := (time.Now().UnixMilli() - m.Timestamp) + if timeDelta < 0 { + timeDelta = -timeDelta + } + if timeDelta > 9999 { + log.Println(err) + ws.Close(websocket.StatusInvalidFramePayloadData, "Your clock is off by too much") + break + } + + book.Send(repeaterName, m) + } + + log.Println(client, repeaterName, "disconnect") } func main() { @@ -57,7 +153,7 @@ func main() { port = "8080" } log.Println("Listening on port", port) - err := http.ListenAndServe(":" + port, nil) + err := http.ListenAndServe(":"+port, nil) if err != nil { log.Fatal(err.Error()) } diff --git a/cmd/vail/message.go b/cmd/vail/message.go index c9d80db..eb98270 100644 --- a/cmd/vail/message.go +++ b/cmd/vail/message.go @@ -6,32 +6,49 @@ import ( "time" ) +// MessageSender can send Messages +type MessageSender interface { + Send(m Message) error +} + +// MessageReceiver can receive Messages +type MessageReceiver interface { + Receive() (Message, error) +} + +// MessageSocket can send and receive Messages +type MessageSocket interface { + MessageSender + MessageReceiver +} + // VailMessage is a single Vail message. type Message struct { - // Relative time in ms of this message. - // These timestamps need to be consistent, but the offset can be anything. - // ECMAScript `performance.now()` is ideal. + // Timestamp of this message. Milliseconds since epoch. Timestamp int64 - + + // Number of connected clients + Clients uint16 + // Message timing in ms. // Timings alternate between tone and silence. // For example, `A` could be sent as [80, 80, 240] - Duration []uint8 + Duration []uint16 } -func NewMessage(ts time.Time, durations []time.Duration) Message { +func NewMessage(ts time.Time, durations ...time.Duration) Message { msg := Message{ Timestamp: ts.UnixNano() / time.Millisecond.Nanoseconds(), - Duration: make([]uint8, len(durations)), + Duration: make([]uint16, len(durations)), } for i, dns := range durations { ms := dns.Milliseconds() - if (ms > 255) { + if ms > 255 { ms = 255 - } else if (ms < 0) { + } else if ms < 0 { ms = 0 } - msg.Duration[i] = uint8(ms) + msg.Duration[i] = uint16(ms) } return msg } @@ -42,20 +59,26 @@ func (m Message) MarshalBinary() ([]byte, error) { if err := binary.Write(&w, binary.BigEndian, m.Timestamp); err != nil { return nil, err } + if err := binary.Write(&w, binary.BigEndian, m.Clients); err != nil { + return nil, err + } if err := binary.Write(&w, binary.BigEndian, m.Duration); err != nil { return nil, err } return w.Bytes(), nil } -// Unmarshaling presumes something else is keeping track of lengths +// UnmarshalBinary unpacks a binary buffer into a Message. func (m *Message) UnmarshalBinary(data []byte) error { r := bytes.NewReader(data) if err := binary.Read(r, binary.BigEndian, &m.Timestamp); err != nil { return err } - dlen := r.Len() - m.Duration = make([]uint8, dlen) + if err := binary.Read(r, binary.BigEndian, &m.Clients); err != nil { + return err + } + dlen := r.Len() / 2 + m.Duration = make([]uint16, dlen) if err := binary.Read(r, binary.BigEndian, &m.Duration); err != nil { return err } @@ -66,16 +89,16 @@ func (m Message) Equal(m2 Message) bool { if m.Timestamp != m2.Timestamp { return false } - + if len(m.Duration) != len(m2.Duration) { return false } - + for i := range m.Duration { if m.Duration[i] != m2.Duration[i] { return false } } - + return true } diff --git a/cmd/vail/message_test.go b/cmd/vail/message_test.go index 94354c6..b88e21e 100644 --- a/cmd/vail/message_test.go +++ b/cmd/vail/message_test.go @@ -6,9 +6,9 @@ import ( "time" ) -func TestMessage(t *testing.T) { - m := Message{0x1122334455, []uint8{0xaa, 0xbb, 0xcc}} - m2 := Message{12, []uint8{1}} +func TestMessageStruct(t *testing.T) { + m := Message{0x1122334455, 0, []uint16{0xaa, 0xbb, 0xcc}} + m2 := Message{12, 0, []uint16{1}} if !m.Equal(m) { t.Error("Equal messages did not compare equal") @@ -16,7 +16,7 @@ func TestMessage(t *testing.T) { if m.Equal(m2) { t.Error("Unequal messages compared equal") } - if m.Equal(Message{m.Timestamp, []uint8{1, 2, 3}}) { + if m.Equal(Message{m.Timestamp, 0, []uint16{1, 2, 3}}) { t.Error("Messages with different payloads compared equal") } @@ -24,7 +24,7 @@ func TestMessage(t *testing.T) { if err != nil { t.Error(err) } - if !bytes.Equal(bm, []byte("\x00\x00\x00\x11\x22\x33\x44\x55\xaa\xbb\xcc")) { + if !bytes.Equal(bm, []byte("\x00\x00\x00\x11\x22\x33\x44\x55\x00\x00\x00\xaa\x00\xbb\x00\xcc")) { t.Error("Encoded wrong:", bm) } @@ -40,11 +40,9 @@ func TestMessage(t *testing.T) { 0, m.Timestamp*time.Millisecond.Nanoseconds(), ), - []time.Duration{ - time.Duration(m.Duration[0]) * time.Millisecond, - time.Duration(m.Duration[1]) * time.Millisecond, - time.Duration(m.Duration[2]) * time.Millisecond, - }, + time.Duration(m.Duration[0])*time.Millisecond, + time.Duration(m.Duration[1])*time.Millisecond, + time.Duration(m.Duration[2])*time.Millisecond, ) if !m.Equal(m3) { t.Error("NewMessage didn't work", m, m3) diff --git a/cmd/vail/repeater.go b/cmd/vail/repeater.go index c0366f5..34fad55 100644 --- a/cmd/vail/repeater.go +++ b/cmd/vail/repeater.go @@ -1,40 +1,56 @@ package main import ( - "io" + "time" ) -// A Repeater is just a list of Writers. +// A Repeater is just a list of senders. type Repeater struct { - writers []io.Writer + clock Clock + senders []MessageSender } +// NewRepeater returns a newly-created repeater func NewRepeater() *Repeater { return &Repeater{ - writers: make([]io.Writer, 0, 20), + clock: WallClock{}, + senders: make([]MessageSender, 0, 20), } } -func (r *Repeater) Join(w io.Writer) { - r.writers = append(r.writers, w) +// Join joins a writer to this repeater +func (r *Repeater) Join(sender MessageSender) { + r.senders = append(r.senders, sender) + r.SendMessage() } -func (r *Repeater) Part(w io.Writer) { - for i, s := range r.writers { - if s == w { - nsubs := len(r.writers) - r.writers[i] = r.writers[nsubs-1] - r.writers = r.writers[:nsubs-1] +// Part removes a writer from this repeater +func (r *Repeater) Part(sender MessageSender) { + for i, s := range r.senders { + if s == sender { + nsubs := len(r.senders) + r.senders[i] = r.senders[nsubs-1] + r.senders = r.senders[:nsubs-1] } } + r.SendMessage() } -func (r *Repeater) Send(p []byte) { - for _, s := range r.writers { - s.Write(p) +// Send send a message to all connected clients +func (r *Repeater) Send(m Message) { + m.Clients = uint16(r.Listeners()) + for _, s := range r.senders { + s.Send(m) } } -func (r *Repeater) Listeners() int { - return len(r.writers) +// SendMessage constructs and sends a message +func (r *Repeater) SendMessage(durations ...time.Duration) { + m := NewMessage(r.clock.Now(), durations...) + r.Send(m) +} + +// Listeners returns the number of connected clients +func (r *Repeater) Listeners() int { + return len(r.senders) } diff --git a/cmd/vail/repeater_test.go b/cmd/vail/repeater_test.go index c09d5f0..383e33b 100644 --- a/cmd/vail/repeater_test.go +++ b/cmd/vail/repeater_test.go @@ -1,39 +1,85 @@ package main import ( - "bytes" "testing" + "time" ) -func TestRepeater(t *testing.T) { - r := NewRepeater() +type FakeClock struct{} - buf1 := bytes.NewBufferString("buf1") - r.Join(buf1) - if r.Listeners() != 1 { - t.Error("Joining did nothing") - } - r.Send([]byte("moo")) - if buf1.String() != "buf1moo" { - t.Error("Client 1 not repeating", buf1) - } +func (f FakeClock) Now() time.Time { + return time.UnixMilli(0) +} - buf2 := bytes.NewBufferString("buf2") - r.Join(buf2) - r.Send([]byte("bar")) - if buf1.String() != "buf1moobar" { - t.Error("Client 1 not repeating", buf1) - } - if buf2.String() != "buf2bar" { - t.Error("Client 2 not repeating", buf2) - } +type TestingClient struct { + buf []Message + expected []Message + t *testing.T +} - r.Part(buf1) - r.Send([]byte("baz")) - if buf1.String() != "buf1moobar" { - t.Error("Client 1 still getting data after part", buf1) - } - if buf2.String() != "buf2barbaz" { - t.Error("Client 2 not getting data after part", buf2) +func NewTestingClient(t *testing.T) *TestingClient { + return &TestingClient{ + t: t, + } +} + +func (tc *TestingClient) Send(m Message) error { + tc.buf = append(tc.buf, m) + return nil +} + +func (tc *TestingClient) Len() int { + return len(tc.buf) +} + +func (tc *TestingClient) Expect(clients uint16, payload ...uint16) { + m := Message{0, clients, payload} + tc.expected = append(tc.expected, m) + if len(tc.buf) != len(tc.expected) { + tc.t.Errorf("Client buffer mismatch. Wanted length %d, got length %d", len(tc.expected), len(tc.buf)) + } + for i := 0; i < len(tc.buf); i++ { + if !tc.buf[i].Equal(tc.expected[i]) { + tc.t.Errorf("Client buffer mismatch at entry %d. Wanted %#v, got %#v", i, tc.expected[i], tc.buf[i]) + } + } + + tc.buf = []Message{} + tc.expected = []Message{} +} + +func NewTestingRepeater() *Repeater { + return &Repeater{ + clock: FakeClock{}, + senders: make([]MessageSender, 0, 2), + } +} + +func TestRepeater(t *testing.T) { + r := NewTestingRepeater() + + c1 := NewTestingClient(t) + r.Join(c1) + c1.Expect(1) + + r.SendMessage(15 * time.Millisecond) + c1.Expect(1, 15) + + c2 := NewTestingClient(t) + r.Join(c2) + c1.Expect(2) + c2.Expect(2) + + r.SendMessage(58 * time.Millisecond) + c1.Expect(2, 58) + c2.Expect(2, 58) + + r.Part(c1) + c2.Expect(1) + + r.SendMessage(5 * time.Millisecond) + c2.Expect(1, 5) + if c1.Len() > 0 { + t.Error("Client 1 still getting data after part") } } diff --git a/go.mod b/go.mod index 0400ad7..ca07540 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/nealey/vail go 1.12 -require golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 +require ( + golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd // indirect + nhooyr.io/websocket v1.8.7 +) diff --git a/go.sum b/go.sum index e679e7c..0ceebe3 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,66 @@ -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 h1:WQ8q63x+f/zpC8Ac1s9wLElVoHhm32p6tudrU72n1QA= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= diff --git a/static/index.html b/static/index.html index eb43d7e..d7ea930 100644 --- a/static/index.html +++ b/static/index.html @@ -46,7 +46,7 @@