]> Sergey Matveev's repositories - vors.git/commitdiff
Rooms support
authorSergey Matveev <stargrave@stargrave.org>
Sat, 13 Apr 2024 19:38:26 +0000 (22:38 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Sat, 13 Apr 2024 20:16:07 +0000 (23:16 +0300)
cmd/client/gui.go
cmd/client/main.go
cmd/server/gui.go
cmd/server/main.go
cmd/server/peer.go
cmd/server/room.go [new file with mode: 0644]
doc/features.texi [new file with mode: 0644]
doc/index.texi
doc/usage.texi

index 5e637bcd0fee4bf26dc6f8dae0f035e60a7416fef0cf16bd77d7d461a53900e7..cb79c76e04685667fe360bc201d0d8670d3da2943401352dbe0df98ad19376a1 100644 (file)
@@ -16,6 +16,7 @@
 package main
 
 import (
+       "fmt"
        "sort"
 
        "github.com/jroimartin/gocui"
@@ -48,7 +49,7 @@ func guiLayout(gui *gocui.Gui) error {
                if err != gocui.ErrUnknownView {
                        return err
                }
-               v.Title = "Logs"
+               v.Title = fmt.Sprintf("Logs name=%s room=%s", *Name, *Room)
                v.Autoscroll = true
        }
        sids := make([]int, 0, len(Streams))
index c0fc01550aa9a39223a50b3cdff480c22e826f3b2ab8bbcc0b930ac7d5736ca6..45a5a67151ad8e6df2732df5a1b43cde314471843f5c5a81029d00115165dc0b 100644 (file)
@@ -51,6 +51,7 @@ var (
        Finish   = make(chan struct{})
        OurStats = &Stats{dead: make(chan struct{})}
        Name     = flag.String("name", "test", "Username")
+       Room     = flag.String("room", "/", "Room name")
        Muted    bool
 )
 
@@ -92,6 +93,7 @@ func main() {
        recCmd := flag.String("rec", "rec "+soxParams, "rec command")
        playCmd := flag.String("play", "play "+soxParams, "play command")
        vadRaw := flag.Uint("vad", 0, "VAD threshold")
+       passwd := flag.String("passwd", "", "Protected room's password")
        flag.Parse()
        log.SetFlags(log.Lmicroseconds | log.Lshortfile)
 
@@ -99,6 +101,7 @@ func main() {
        if err != nil {
                log.Fatal(err)
        }
+       *Name = strings.ReplaceAll(*Name, " ", "-")
 
        vad := uint64(*vadRaw)
        opusEnc := newOpusEnc()
@@ -138,7 +141,7 @@ func main() {
        if err != nil {
                log.Fatalln("noise.NewHandshakeState:", err)
        }
-       buf, _, _, err := hs.WriteMessage(nil, []byte(*Name))
+       buf, _, _, err := hs.WriteMessage(nil, []byte(*Name+" "+*Room+" "+*passwd))
        if err != nil {
                log.Fatalln("handshake encrypt:", err)
        }
index 0e488e8977a420a654040eb7d1114c86a180c9fd98dfde0ce0a6e3016b81d743..6e16d02a8a911a373c67bdf139088ebda1b717a46978f53b51a16ea2b08f860e 100644 (file)
@@ -19,6 +19,7 @@ import (
        "flag"
        "os"
        "sort"
+       "strings"
        "time"
 
        "github.com/jroimartin/gocui"
@@ -51,20 +52,28 @@ func guiLayout(gui *gocui.Gui) error {
                v.Title = "Logs"
                v.Autoscroll = true
        }
-       sids := make([]int, 0, len(Peers))
-       for sid := range Peers {
-               sids = append(sids, int(sid))
+       roomNames := make([]string, 0, len(Rooms))
+       for n := range Rooms {
+               roomNames = append(roomNames, n)
        }
-       sort.Ints(sids)
-       for _, sid := range sids {
-               peer := Peers[byte(sid)]
-               v, err := gui.SetView(peer.name, 0, prevY, maxX-1, prevY+2)
-               prevY += 3
+       sort.Strings(roomNames)
+       var now time.Time
+       for _, name := range roomNames {
+               room := Rooms[name]
+               lines := room.Stats(now)
+               v, err = gui.SetView(room.name, 0, prevY, maxX-1, prevY+1+len(lines))
+               prevY += 2 + len(lines)
                if err != nil {
                        if err != gocui.ErrUnknownView {
                                return err
                        }
-                       v.Title = peer.name
+                       title := room.name
+                       if room.key != "" {
+                               title += " protected"
+                       }
+                       v.Title = title
+                       v.Clear()
+                       v.Write([]byte(strings.Join(lines, "\n")))
                }
        }
        if !GUIReady {
index 86204b86ffca078a9e861369fe11c94b5c9a665c0256473476a4973af158e563..37efdfbc779ca0f439de16289649c90a75fd573b4d8eb9f780daa9c6170ec2a6 100644 (file)
@@ -29,10 +29,9 @@ import (
        "net/netip"
        "os"
        "strconv"
-       "sync"
+       "strings"
        "time"
 
-       "github.com/dustin/go-humanize"
        "github.com/flynn/noise"
        "github.com/jroimartin/gocui"
        vors "go.stargrave.org/vors/internal"
@@ -46,8 +45,6 @@ var (
                MinVersion:       tls.VersionTLS13,
                CurvePreferences: []tls.CurveID{tls.X25519},
        }
-       Peers    = map[byte]*Peer{}
-       PeersM   sync.Mutex
        Prv, Pub []byte
        Cookies  = map[vors.Cookie]chan *net.UDPAddr{}
 )
@@ -56,10 +53,6 @@ func newPeer(conn *net.TCPConn) {
        logger := slog.With("remote", conn.RemoteAddr().String())
        logger.Info("connected")
        defer conn.Close()
-       if len(Peers) == 1<<8 {
-               logger.Error("too many peers")
-               return
-       }
        err := conn.SetNoDelay(true)
        if err != nil {
                log.Fatalln("nodelay:", err)
@@ -90,25 +83,78 @@ func newPeer(conn *net.TCPConn) {
                logger.Error("read handshake", "err", err)
                return
        }
-       peer := Peer{
+       peer := &Peer{
                logger: logger,
                conn:   conn,
-               stats:  &Stats{alive: make(chan struct{})},
+               stats:  &Stats{},
                rx:     make(chan []byte),
                tx:     make(chan []byte, 10),
                alive:  make(chan struct{}),
        }
+       var room *Room
        {
-               name, _, _, err := hs.ReadMessage(nil, buf)
+               nameAndRoom, _, _, err := hs.ReadMessage(nil, buf)
                if err != nil {
                        logger.Error("handshake: decrypt", "err", err)
                        return
                }
-               peer.name = string(name)
+               cols := strings.SplitN(string(nameAndRoom), " ", 3)
+               roomName := "/"
+               if len(cols) > 1 {
+                       roomName = cols[1]
+               }
+               var key string
+               if len(cols) > 2 {
+                       key = cols[2]
+               }
+               peer.name = string(cols[0])
+               logger = logger.With("name", peer.name, "room", roomName)
+               RoomsM.Lock()
+               room = Rooms[roomName]
+               if room == nil {
+                       room = &Room{
+                               name:  roomName,
+                               key:   key,
+                               peers: make(map[byte]*Peer),
+                               alive: make(chan struct{}),
+                       }
+                       Rooms[roomName] = room
+                       go func() {
+                               if *NoGUI {
+                                       return
+                               }
+                               tick := time.Tick(vors.ScreenRefresh)
+                               var now time.Time
+                               var v *gocui.View
+                               for {
+                                       select {
+                                       case <-room.alive:
+                                               GUI.DeleteView(room.name)
+                                               return
+                                       case now = <-tick:
+                                               v, err = GUI.View(room.name)
+                                               if err == nil {
+                                                       v.Clear()
+                                                       v.Write([]byte(strings.Join(room.Stats(now), "\n")))
+                                               }
+                                       }
+                               }
+                       }()
+               }
+               RoomsM.Unlock()
+               if room.key != key {
+                       logger.Error("wrong password")
+                       buf, _, _, err = hs.WriteMessage(nil, []byte("wrong password"))
+                       if err != nil {
+                               log.Fatal(err)
+                       }
+                       vors.PktWrite(conn, buf)
+                       return
+               }
        }
-       logger = logger.With("name", peer.name)
+       peer.room = room
 
-       for _, p := range Peers {
+       for _, p := range room.peers {
                if p.name != peer.name {
                        continue
                }
@@ -134,7 +180,7 @@ func newPeer(conn *net.TCPConn) {
                        }
                }
                if found {
-                       Peers[peer.sid] = &peer
+                       Peers[peer.sid] = peer
                        go peer.Tx()
                }
                PeersM.Unlock()
@@ -148,16 +194,17 @@ func newPeer(conn *net.TCPConn) {
                }
        }
        logger = logger.With("sid", peer.sid)
+       room.peers[peer.sid] = peer
        logger.Info("logged in")
 
        defer func() {
                logger.Info("removing")
                PeersM.Lock()
                delete(Peers, peer.sid)
+               delete(room.peers, peer.sid)
                PeersM.Unlock()
-               close(peer.stats.alive)
                s := []byte(fmt.Sprintf("%s %d", vors.CmdDel, peer.sid))
-               for _, p := range Peers {
+               for _, p := range room.peers {
                        go func(tx chan []byte) { tx <- s }(p.tx)
                }
        }()
@@ -200,7 +247,7 @@ func newPeer(conn *net.TCPConn) {
        go peer.Rx()
        peer.tx <- []byte(fmt.Sprintf("SID %d", peer.sid))
 
-       for _, p := range Peers {
+       for _, p := range room.peers {
                if p.sid == peer.sid {
                        continue
                }
@@ -220,7 +267,7 @@ func newPeer(conn *net.TCPConn) {
        {
                s := []byte(fmt.Sprintf("%s %d %s %s",
                        vors.CmdAdd, peer.sid, peer.name, hex.EncodeToString(peer.key)))
-               for _, p := range Peers {
+               for _, p := range room.peers {
                        if p.sid != peer.sid {
                                p.tx <- s
                        }
@@ -245,40 +292,6 @@ func newPeer(conn *net.TCPConn) {
                }
        }(&seen)
 
-       go func(stats *Stats) {
-               if *NoGUI {
-                       return
-               }
-               tick := time.Tick(vors.ScreenRefresh)
-               var now time.Time
-               var v *gocui.View
-               for {
-                       select {
-                       case <-stats.alive:
-                               GUI.DeleteView(peer.name)
-                               return
-                       case now = <-tick:
-                               s := fmt.Sprintf(
-                                       "%s | Rx/Tx/Bad: %s / %s / %s |  %s / %s",
-                                       peer.addr,
-                                       humanize.Comma(stats.pktsRx),
-                                       humanize.Comma(stats.pktsTx),
-                                       humanize.Comma(stats.bads),
-                                       humanize.IBytes(stats.bytesRx),
-                                       humanize.IBytes(stats.bytesTx),
-                               )
-                               if stats.last.Add(vors.ScreenRefresh).After(now) {
-                                       s += "  |  " + vors.CGreen + "TALK" + vors.CReset
-                               }
-                               v, err = GUI.View(peer.name)
-                               if err == nil {
-                                       v.Clear()
-                                       v.Write([]byte(s))
-                               }
-                       }
-               }
-       }(peer.stats)
-
        for buf := range peer.rx {
                if string(buf) == vors.CmdPing {
                        seen = time.Now()
@@ -424,7 +437,7 @@ func main() {
                        }
 
                        peer.stats.last = time.Now()
-                       for _, p := range Peers {
+                       for _, p := range peer.room.peers {
                                if p.sid == sid {
                                        continue
                                }
index 960d5b2dbbd225fb38e3b75d8b48415ca5db851368d359b25adbd3fecc0af8d1..d1e078a9491a8bca2a67896c008b2ffa89e0c8312148fb3500c647d1277418c5 100644 (file)
@@ -10,6 +10,11 @@ import (
        vors "go.stargrave.org/vors/internal"
 )
 
+var (
+       Peers  = map[byte]*Peer{}
+       PeersM sync.Mutex
+)
+
 type Stats struct {
        pktsRx  int64
        pktsTx  int64
@@ -17,7 +22,6 @@ type Stats struct {
        bytesTx uint64
        bads    int64
        last    time.Time
-       alive   chan struct{}
 }
 
 type Peer struct {
@@ -26,6 +30,7 @@ type Peer struct {
        addr  *net.UDPAddr
        key   []byte
        stats *Stats
+       room  *Room
 
        logger     *slog.Logger
        conn       net.Conn
diff --git a/cmd/server/room.go b/cmd/server/room.go
new file mode 100644 (file)
index 0000000..8d88406
--- /dev/null
@@ -0,0 +1,53 @@
+package main
+
+import (
+       "fmt"
+       "sort"
+       "sync"
+       "time"
+
+       "github.com/dustin/go-humanize"
+       vors "go.stargrave.org/vors/internal"
+)
+
+var (
+       Rooms  = map[string]*Room{}
+       RoomsM sync.Mutex
+)
+
+type Room struct {
+       name  string
+       key   string
+       peers map[byte]*Peer
+       alive chan struct{}
+}
+
+func (room *Room) Stats(now time.Time) []string {
+       sids := make([]int, 0, len(room.peers))
+       for sid := range room.peers {
+               sids = append(sids, int(sid))
+       }
+       sort.Ints(sids)
+       lines := make([]string, 0, len(sids))
+       for _, sid := range sids {
+               peer := room.peers[byte(sid)]
+               if peer == nil {
+                       continue
+               }
+               line := fmt.Sprintf(
+                       "%12s  |  %s | Rx/Tx/Bad: %s / %s / %s |  %s / %s",
+                       peer.name,
+                       peer.addr,
+                       humanize.Comma(peer.stats.pktsRx),
+                       humanize.Comma(peer.stats.pktsTx),
+                       humanize.Comma(peer.stats.bads),
+                       humanize.IBytes(peer.stats.bytesRx),
+                       humanize.IBytes(peer.stats.bytesTx),
+               )
+               if peer.stats.last.Add(vors.ScreenRefresh).After(now) {
+                       line += "  |  " + vors.CGreen + "TALK" + vors.CReset
+               }
+               lines = append(lines, line)
+       }
+       return lines
+}
diff --git a/doc/features.texi b/doc/features.texi
new file mode 100644 (file)
index 0000000..8987ded
--- /dev/null
@@ -0,0 +1,22 @@
+@node Features
+@unnumbered Features
+
+@itemize
+
+@item Client-server architecture. All clients send their output to the
+server, while it copies it to other clients. However, as a rule, there
+is single client speaking at one time.
+
+@item 20ms frames with  Opus-encoded audio with PLC (Packet Loss
+Concealment) and DTX (discontinuous transmission) features enabled.
+Optional VAD (voice activity detection).
+
+@item Noise protocol-based handshake over TCP between client and server
+for creating authenticated encrypted channel and authentication based on
+server's public key knowledge.
+
+@item Rooms, optionally password protected.
+
+@item Fancy TUI client with mute-toggle ability.
+
+@end itemize
index 29be2ed38fe639e527904c65ccfe4da954577259ee51d1b5c75d715b3c3be292..06185d37a9c9814375c74796ff3656e94f867fec4570d43e70e6b3a55c93268a 100644 (file)
@@ -12,11 +12,6 @@ VoRS -- Vo(IP) Really Simple.
 Very simple and usable multi-user VoIP solution.
 Some kind of alternative to @url{https://www.mumble.info/, Mumble}.
 
-@float
-@image{screenshots/example,,,Server and two clients,.webp}
-@caption{Server (above) and two clients (left and right) in a terminal multiplexer}
-@end float
-
 But why? SIP-based solutions are pretty complicated to setup, because
 they are not made for simple tasks like sudden voice chats between a
 few people. WebRTC-based solutions are insane bloated incredible
@@ -44,15 +39,21 @@ Mumble tends to output no information, sometimes hiding the fact of a
 problem and that everything stopped working.
 
 @item No NAT-traversal possibility. It is the year 2024 year already,
-stop trying to use and revive legacy obsolete IPv4. Or use some overlay
-network on top of it, Or VPN, whatever.
+stop trying to use and revive legacy obsolete IPv4. Either use some
+overlay network on top of it, or VPN, whatever.
 
 @item Mono-cypher, mono-codec protocol. The @url{https://opus-codec.org/, Opus}
 audio codec is perfect for VoIP tasks. ChaCha20-Poly1305 is more than
 appropriate and satisfies as fast and secure encryption solution.
 
+@float
+@image{screenshots/example,,,Server and two clients,.webp}
+@caption{Server (above) and two clients (left and right) in a terminal multiplexer}
+@end float
+
 @end itemize
 
+@include features.texi
 @include install.texi
 @include usage.texi
 @include vad.texi
index 396685ce1d6ff798b1f2fa5db6576eb3cc22d322e2f6f0319ed2f1de6bf1813d..84e566096e2626bf47a51dcf83fdbdac2dacca8d713f9e67ab6dd8caee9db3bc 100644 (file)
@@ -35,4 +35,8 @@ $ vors-client -srv "[2001:db8::1]:12978" -pub $pub -name NAME
     Pressing F10 in server/client TUIs means quitting. Pressing Enter in
     client means "mute" toggling.
 
+@item
+    @option{-room} allows you to join non-root room.
+    @option{-passwd} allows you to protect with provided password.
+
 @end itemize