diff --git a/cmd/vail/book_test.go b/cmd/vail/book_test.go index aa66412..1026832 100644 --- a/cmd/vail/book_test.go +++ b/cmd/vail/book_test.go @@ -20,7 +20,7 @@ func TestBook(t *testing.T) { c1.Expect(1) // Send to an empty channel - m := Message{0, 0, []uint8{22, 33}} + m := Message{0, 0, []uint16{22, 33}} b.Send("merf", m) b.loop() if c1.Len() > 0 { diff --git a/cmd/vail/main.go b/cmd/vail/main.go index 27d8a61..6fcc544 100644 --- a/cmd/vail/main.go +++ b/cmd/vail/main.go @@ -1,11 +1,11 @@ package main import ( - "fmt" + "context" + "encoding/json" "log" "net/http" "os" - "strings" "time" "nhooyr.io/websocket" @@ -13,6 +13,9 @@ import ( var book Book +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 @@ -36,51 +39,82 @@ type VailWebSocketConnection struct { func (c *VailWebSocketConnection) Receive() (Message, error) { var m Message - var err error - if c.usingJSON { - err = websocket.JSON.Receive(c.Conn, &m) + messageType, buf, err := c.Read(context.Background()) + if err != nil { + return m, err + } + + if messageType == websocket.MessageText { + err = json.Unmarshal(buf, &m) } else { - buf := make([]byte, 64) - if err := websocket.Message.Receive(c.Conn, &buf); err != nil { - return m, err - } - if err := m.UnmarshalBinary(buf) + err = m.UnmarshalBinary(buf) } return m, err } func (c *VailWebSocketConnection) Send(m Message) error { + var err error + var buf []byte + var messageType websocket.MessageType + + log.Println("Send", m) if c.usingJSON { - return websocket.JSON.Send(c.Conn, m) + messageType = websocket.MessageText + buf, err = json.Marshal(m) } else { - return websocket.Message.Send(c.Conn, m) + messageType = websocket.MessageBinary + buf, err = m.MarshalBinary() } -} - -func (c *VailWebSocketConnection) Error(err error) { - msg := fmt.Sprintf("Error: %#v", err) - websocket.JSON.Send(c.Conn, msg) -} - -type Client struct { - repeaterName string - usingJSON bool -} - -func (c Client) Handle(ws *websocket.Conn) { - sock := &VailWebSocketConnection{ - Conn: ws, - usingJSON: c.usingJSON, + log.Println(buf, err) + if err != nil { + return err } - nowMilli := time.Now().UnixMilli() - ws.MaxPayloadBytes = 50 - book.Join(c.repeaterName, sock) - defer book.Part(c.repeaterName, sock) + + log.Println("Sending") + return c.Write(context.Background(), messageType, buf) +} + +func ChatHandler(w http.ResponseWriter, r *http.Request) { + // 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, + } + + // 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) for { + // Read a packet m, err := sock.Receive() if err != nil { - sock.Error(err) + log.Println(err) + ws.Close(websocket.StatusInvalidFramePayloadData, err.Error()) break } @@ -90,34 +124,20 @@ func (c Client) Handle(ws *websocket.Conn) { } // If it's wildly out of time, reject it - timeDelta := (nowMilli - m.Timestamp) + timeDelta := (time.Now().UnixMilli() - m.Timestamp) if timeDelta < 0 { timeDelta = -timeDelta } if timeDelta > 9999 { - fmt.Fprintln(ws, "Bad timestamp") - ws.Close() - return + log.Println(err) + ws.Close(websocket.StatusInvalidFramePayloadData, "Your clock is off by too much") + break } - book.Send(c.repeaterName, m) + book.Send(repeaterName, m) } } -func ChatHandler(w http.ResponseWriter, r *http.Request) { - c := Client{ - repeaterName: r.FormValue("repeater"), - } - accept := r.Header.Get("Accept") - if strings.Contains(accept, "json") { - c.usingJSON = true - } - - // This API is confusing as hell. - // I suspect there's a better way to do this. - websocket.Handler(c.Handle).ServeHTTP(w, r) -} - func main() { book = NewBook() http.Handle("/chat", http.HandlerFunc(ChatHandler)) diff --git a/cmd/vail/message.go b/cmd/vail/message.go index 0b448b1..ef036e4 100644 --- a/cmd/vail/message.go +++ b/cmd/vail/message.go @@ -34,13 +34,13 @@ type Message struct { // 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 { 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() @@ -49,7 +49,7 @@ func NewMessage(ts time.Time, durations ...time.Duration) Message { } else if ms < 0 { ms = 0 } - msg.Duration[i] = uint8(ms) + msg.Duration[i] = uint16(ms) } return msg } @@ -69,7 +69,7 @@ func (m Message) MarshalBinary() ([]byte, error) { 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 { @@ -78,32 +78,14 @@ func (m *Message) UnmarshalBinary(data []byte) error { if err := binary.Read(r, binary.BigEndian, &m.Clients); err != nil { return err } - dlen := r.Len() - m.Duration = make([]uint8, dlen) + dlen := r.Len() / 2 + m.Duration = make([]uint16, dlen) if err := binary.Read(r, binary.BigEndian, &m.Duration); err != nil { return err } return nil } -func (m Message) MarshalJSON() ([]byte, error) { - buf := new(bytes.Buffer) - fmt.Fprint(buf, "{") - fmt.Fprintf(buf, "\"Timestamp\":%d,", m.Timestamp) - fmt.Fprintf(buf, "\"Clients\":%d,", m.Clients) - fmt.Fprint(buf, "\"Duration\":[") - for i := 0; i < len(m.Duration); i++ { - fmt.Fprint(buf, m.Duration[i]) - if i <= len(m.Duration)-1 { - fmt.Fprint(buf, ",") - } - } - fmt.Fprint(buf) - fmt.Fprint(buf, "]") - fmt.Fprint(buf, "}") - return buf.Bytes(), nil -} - func (m Message) Equal(m2 Message) bool { if m.Timestamp != m2.Timestamp { return false diff --git a/cmd/vail/message_test.go b/cmd/vail/message_test.go index dae2d44..b88e21e 100644 --- a/cmd/vail/message_test.go +++ b/cmd/vail/message_test.go @@ -7,8 +7,8 @@ import ( ) func TestMessageStruct(t *testing.T) { - m := Message{0x1122334455, 0, []uint8{0xaa, 0xbb, 0xcc}} - m2 := Message{12, 0, []uint8{1}} + 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 TestMessageStruct(t *testing.T) { if m.Equal(m2) { t.Error("Unequal messages compared equal") } - if m.Equal(Message{m.Timestamp, 0, []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 TestMessageStruct(t *testing.T) { if err != nil { t.Error(err) } - if !bytes.Equal(bm, []byte("\x00\x00\x00\x11\x22\x33\x44\x55\x00\x00\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) } diff --git a/cmd/vail/repeater_test.go b/cmd/vail/repeater_test.go index 16136fc..383e33b 100644 --- a/cmd/vail/repeater_test.go +++ b/cmd/vail/repeater_test.go @@ -32,7 +32,7 @@ func (tc *TestingClient) Len() int { return len(tc.buf) } -func (tc *TestingClient) Expect(clients uint16, payload ...uint8) { +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) { diff --git a/static/repeaters.mjs b/static/repeaters.mjs index a17a32f..c943801 100644 --- a/static/repeaters.mjs +++ b/static/repeaters.mjs @@ -45,13 +45,13 @@ export class Vail { } console.info("Attempting to reconnect", this.wsUrl.href) this.clockOffset = 0 - this.socket = new WebSocket(this.wsUrl) + this.socket = new WebSocket(this.wsUrl, ["json.vail.woozle.org"]) this.socket.addEventListener("message", e => this.wsMessage(e)) this.socket.addEventListener( - "close", - () => { - console.info("Repeater connection dropped.") - setTimeout(() => this.reopen(), 5*Second) + "close", + msg => { + console.error("Repeater connection dropped:", msg.reason) + setTimeout(() => this.reopen(), 2*Second) } ) } @@ -72,6 +72,7 @@ export class Vail { clockOffset: this.clockOffset, clients: msg.Clients, } + console.log(msg) if (typeof(msg) == "string") { console.error(msg) return