summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorbt <bt@rctt.net>2026-04-19 21:32:53 +0200
committerbt <bt@rctt.net>2026-05-03 17:52:37 +0200
commiteec10d41af62fb9a93cd5fd79dcf94616701cc2a (patch)
treed72068dcc4cb1aa43c2e0a2fae8ff094d41ed9c6
parentc00e7dd589921e6be45918d4cd589e52e2b77036 (diff)
downloadsolec-0.3.0.tar.gz
solec-0.3.0.zip
[common] Basic group channels supportv0.3.0
-rw-r--r--client/client.go20
-rw-r--r--cmd/client/main.go16
-rw-r--r--cmd/daemon/main.go1
-rw-r--r--core/data.go51
-rw-r--r--core/payload.go35
-rw-r--r--docs/rfc.md24
-rw-r--r--docs/rfc.txt35
-rw-r--r--server/channel.go48
-rw-r--r--server/message.go40
-rw-r--r--server/remote.go23
-rw-r--r--server/server.go34
-rw-r--r--server/user.go69
-rwxr-xr-xtools/run-two-clients.sh8
13 files changed, 345 insertions, 59 deletions
diff --git a/client/client.go b/client/client.go
index 0fe967f..775fbb6 100644
--- a/client/client.go
+++ b/client/client.go
@@ -63,6 +63,26 @@ func (c *Client) SendMessage(target, content string) error {
return core.Send(c.conn, msg)
}
+func (c *Client) Join(channel string) error {
+ umod := core.Usermode{
+ UserAddr: c.uname + "@" + c.addr,
+ ChannelName: channel,
+ Mode: core.UsermodeInChannel,
+ }
+
+ return core.Send(c.conn, umod)
+}
+
+func (c *Client) Leave(channel string) error {
+ umod := core.Usermode{
+ UserAddr: c.uname + "@" + c.addr,
+ ChannelName: channel,
+ Mode: core.UsermodeNone,
+ }
+
+ return core.Send(c.conn, umod)
+}
+
func (c *Client) read() {
for {
payload, err := core.Read(c.conn)
diff --git a/cmd/client/main.go b/cmd/client/main.go
index 635e7ca..b107d93 100644
--- a/cmd/client/main.go
+++ b/cmd/client/main.go
@@ -30,6 +30,8 @@ func (h *Handler) HandleError(err error) {
func main() {
prompt.Commands["send"] = sendMessage
+ prompt.Commands["join"] = join
+ prompt.Commands["leave"] = leave
flag.StringVar(&serverAddr, "a", "localhost:9999", "Server address:port")
flag.StringVar(&user, "u", "user", "username")
@@ -52,3 +54,17 @@ func sendMessage(args []string) {
log.Println("cannot send message:", err)
}
}
+
+func join(args []string) {
+ err := c.Join(args[0])
+ if err != nil {
+ log.Println("cannot join channel:", err)
+ }
+}
+
+func leave(args []string) {
+ err := c.Leave(args[0])
+ if err != nil {
+ log.Println("cannot leave channel:", err)
+ }
+}
diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go
index 56a319f..bad350f 100644
--- a/cmd/daemon/main.go
+++ b/cmd/daemon/main.go
@@ -29,6 +29,7 @@ func main() {
flag.Parse()
serv = server.NewServer(*addr, *name)
+ serv.AddChannel("test")
go func() {
if err := serv.Start(); err != nil {
diff --git a/core/data.go b/core/data.go
index dd2777f..2a98347 100644
--- a/core/data.go
+++ b/core/data.go
@@ -23,15 +23,17 @@ const (
PayloadUserAuth = 0x04
PayloadMessage = 0x05
PayloadServerAuth = 0x06
+ PayloadUsermode = 0x07
PayloadTest = 0xFF
)
type ErrorType uint8
const (
- ErrorUnknown ErrorType = 0x00
- ErrorAuthFailed = 0x01
- ErrorNotFound = 0x02
+ ErrorUnknown ErrorType = 0x00
+ ErrorAuthFailed = 0x01
+ ErrorNotFound = 0x02
+ ErrorUnauthorized = 0x03
)
type ConnType uint8
@@ -42,6 +44,13 @@ const (
ConnTypeServer = 0x02
)
+type UsermodeType uint8
+
+const (
+ UsermodeNone UsermodeType = 0x00
+ UsermodeInChannel = 0x01
+)
+
type Frame struct {
Length uint16
Type PayloadType
@@ -132,6 +141,8 @@ func Decode(buf io.Reader) (any, error) {
return DecodeServerAuth(buf)
case PayloadMessage:
return DecodeMessage(buf)
+ case PayloadUsermode:
+ return DecodeUsermode(buf)
case PayloadTest:
return DecodeTest(buf)
default:
@@ -190,11 +201,37 @@ func decodeString(buf io.Reader, ptr *string) error {
return nil
}
-func ReadAddr(addr string) (string, string, error) {
- channel, host, ok := strings.Cut(addr, "@")
+type Addr struct {
+ Channel string
+ Host string
+ Type AddrType
+}
+
+type AddrType string
+
+const (
+ AddrUser AddrType = "user"
+ AddrGroup = "group"
+)
+
+func ReadAddr(addrStr string) (Addr, error) {
+ var (
+ addr Addr
+ ok bool
+ )
+
+ addr.Channel, addr.Host, ok = strings.Cut(addrStr, "@")
if !ok {
- return "", "", ErrInvalidAddress
+ return addr, ErrInvalidAddress
+ }
+
+ rest, ok := strings.CutPrefix(addr.Channel, "#")
+ if ok {
+ addr.Channel = rest
+ addr.Type = AddrGroup
+ } else {
+ addr.Type = AddrUser
}
- return channel, host, nil
+ return addr, nil
}
diff --git a/core/payload.go b/core/payload.go
index e943647..3c3afa1 100644
--- a/core/payload.go
+++ b/core/payload.go
@@ -150,6 +150,41 @@ func DecodeMessage(buf io.Reader) (Message, error) {
return m, nil
}
+type Usermode struct {
+ UserAddr string
+ ChannelName string
+ Mode UsermodeType
+}
+
+func (u Usermode) Wrap() (PayloadType, []any) {
+ return PayloadUsermode, []any{
+ u.UserAddr,
+ u.ChannelName,
+ uint8(u.Mode),
+ }
+}
+
+func DecodeUsermode(buf io.Reader) (Usermode, error) {
+ var u Usermode
+
+ err := decodeString(buf, &u.UserAddr)
+ if err != nil {
+ return u, err
+ }
+
+ err = decodeString(buf, &u.ChannelName)
+ if err != nil {
+ return u, err
+ }
+
+ err = decodeNumeric(buf, &u.Mode)
+ if err != nil {
+ return u, err
+ }
+
+ return u, nil
+}
+
type Test struct {
Num1 uint8
Time1 time.Time
diff --git a/docs/rfc.md b/docs/rfc.md
index 69d0e29..3336cd4 100644
--- a/docs/rfc.md
+++ b/docs/rfc.md
@@ -159,9 +159,10 @@ Payload type attributes describes following characteristics:
| 0x01 | Success | SCE |
| 0x02 | Error | S |
| 0x03 | Handshake | SC |
-| 0x04 | UserAuth | C |
+| 0x04 | UserAuth | C |
| 0x05 | Message | SC |
| 0x06 | ServerAuth | S |
+| 0x07 | UserMode | C |
| 0xFF | Test | R |
### Success
@@ -180,6 +181,7 @@ Payload is always empty for this type.
|------|-----------------------------------------------------------|
| 0x01 | Client auth failed. Invalid username or password. |
| 0x02 | Not found. User or channel cannot access user or channel. |
+| 0x03 | Unauthorized. |
### Handshake
@@ -223,6 +225,24 @@ different auth method will be used.
| timestamp | send_time |
| string | message_content |
+### Usermode
+
+Usermode payload is used for changing user settings related to channels. For
+example: joining, leaving or setting privilages.
+
+| Type | Name |
+|--------|--------------|
+| string | user_address |
+| string | channel_name |
+| uint8 | mode |
+
+#### Modes
+
+| Type | Name |
+|------|------------|
+| 0x00 | none |
+| 0x01 | in_channel |
+
### Test
Test payload is used for encoder and decoders testing. Clients and servers
@@ -321,4 +341,4 @@ This is obviously insecure.
Message can be addressed to a single user or a channel.
* user@example.org
-* #channel@example.org \ No newline at end of file
+* #channel@example.org
diff --git a/docs/rfc.txt b/docs/rfc.txt
index 04d931e..b0e42e3 100644
--- a/docs/rfc.txt
+++ b/docs/rfc.txt
@@ -1,7 +1,7 @@
SOLEC Working Group bt, Ed.
Internet-Draft RCTT.net
-Intended status: Experimental 19 April 2026
-Expires: 21 October 2026
+Intended status: Experimental 3 May 2026
+Expires: 4 November 2026
System of Lightweight Electronic Communication
@@ -32,7 +32,8 @@ Table of Contents
2.4.4. UserAuth
2.4.5. ServerAuth
2.4.6. Message
- 2.4.7. Test
+ 2.4.7. Usermode
+ 2.4.8. Test
2.5. Sequential operations
2.6. Client-Server connection initialisation
2.7. Exchanging messages between servers
@@ -175,6 +176,8 @@ Table of Contents
+------+------------+------------+
| 0x06 | ServerAuth | S |
+------+------------+------------+
+ | 0x07 | UserMode | C |
+ +------+------------+------------+
| 0xFF | Test | R |
+------+------------+------------+
@@ -204,6 +207,8 @@ Table of Contents
| 0x02 | Not found. User or channel cannot access user or |
| | channel. |
+------+------------------------------------------------------------+
+ | 0x03 | Unauthorized. |
+ +------+------------------------------------------------------------+
Table 3
@@ -276,7 +281,27 @@ Table of Contents
Table 8
-2.4.7. Test
+2.4.7. Usermode
+
+ Usermode payload is used for changing user settings related to
+ channels. For example: joining, leaving or setting privilages.
+
+ | Type | Name | | string | user_address | | string | channel_name | |
+ uint8 | mode |
+
+2.4.7.1. Modes
+
+ +======+============+
+ | Type | Name |
+ +======+============+
+ | 0x00 | none |
+ +------+------------+
+ | 0x01 | in_channel |
+ +------+------------+
+
+ Table 9
+
+2.4.8. Test
Test payload is used for encoder and decoders testing. Clients and
servers should ignore this kind of payload.
@@ -301,7 +326,7 @@ Table of Contents
| uint64 | num4 |
+-----------+-------+
- Table 9
+ Table 10
2.5. Sequential operations
diff --git a/server/channel.go b/server/channel.go
new file mode 100644
index 0000000..11969f2
--- /dev/null
+++ b/server/channel.go
@@ -0,0 +1,48 @@
+package server
+
+import (
+ "log"
+ "net"
+ "sync"
+
+ "go.rctt.net/solec/core"
+)
+
+type Channel struct {
+ Name string
+ Users map[string]*User
+ UsersMu sync.RWMutex
+}
+
+func NewChannel(name string) *Channel {
+ return &Channel{
+ Name: name,
+ Users: make(map[string]*User),
+ }
+}
+
+func (c *Channel) Add(u *User) {
+ c.UsersMu.Lock()
+ c.Users[u.Name] = u
+ u.Channels[c.Name] = c
+ c.UsersMu.Unlock()
+
+ log.Println("user joined a channel")
+}
+
+func (c *Channel) Remove(u *User) {
+ c.UsersMu.Lock()
+ delete(c.Users, u.Name)
+ delete(u.Channels, c.Name)
+ c.UsersMu.Unlock()
+
+ log.Println("user left a channel")
+}
+
+func (c *Channel) Send(senderConn net.Conn, msg core.Message) {
+ for _, u := range c.Users {
+ if err := u.Send(senderConn, msg); err != nil {
+ log.Print("cannot send a message to user on channel", err)
+ }
+ }
+}
diff --git a/server/message.go b/server/message.go
index 2487abd..58f2908 100644
--- a/server/message.go
+++ b/server/message.go
@@ -1,9 +1,9 @@
package server
import (
- "errors"
"fmt"
"log"
+ "net"
"time"
"go.rctt.net/solec/core"
@@ -30,34 +30,44 @@ func (s *Server) SendBroadcast(msg string) {
}
}
-func (s *Server) handleMessage(msg core.Message) error {
+func (s *Server) handleMessage(sender net.Conn, msg core.Message) error {
log.Println("message:", msg.Source, "->", msg.Target, msg.Content)
- channel, host, err := core.ReadAddr(msg.Target)
+ addr, err := core.ReadAddr(msg.Target)
if err != nil {
return err
}
- if host == s.name {
- return s.handleLocalMessage(channel, msg)
+ if addr.Host == s.name {
+ return s.handleLocalMessage(sender, addr, msg)
}
- return s.handleOutboundMessage(channel, host, msg)
+ return s.handleOutboundMessage(sender, addr, msg)
}
-func (s *Server) handleLocalMessage(channel string, msg core.Message) error {
- s.usersMu.RLock()
- user, ok := s.users[channel]
- if !ok {
- return errors.New("target not found")
+func (s *Server) handleLocalMessage(sender net.Conn, addr core.Addr, msg core.Message) error {
+ if addr.Type == core.AddrUser {
+ s.usersMu.RLock()
+ user, ok := s.users[addr.Channel]
+ if !ok {
+ return core.Send(sender, core.Error{core.ErrorNotFound})
+ }
+ s.usersMu.RUnlock()
+ return user.Send(sender, msg)
}
- s.usersMu.RUnlock()
- return user.Send(msg)
+ s.channelsMu.RLock()
+ channel, ok := s.channels[addr.Channel]
+ if !ok {
+ return core.Send(sender, core.Error{core.ErrorNotFound})
+ }
+ s.channelsMu.RUnlock()
+ channel.Send(sender, msg)
+ return nil
}
-func (s *Server) handleOutboundMessage(channel, host string, msg core.Message) error {
- remote, err := s.getRemote(host)
+func (s *Server) handleOutboundMessage(sender net.Conn, addr core.Addr, msg core.Message) error {
+ remote, err := s.getRemote(addr.Host)
if err != nil {
return fmt.Errorf("cannot access remote server: %w", err)
}
diff --git a/server/remote.go b/server/remote.go
index 2449511..5d86da2 100644
--- a/server/remote.go
+++ b/server/remote.go
@@ -45,7 +45,7 @@ func (s *Server) handleServerConn(conn net.Conn) {
s.serversMu.Unlock()
}()
- if err := s.readInput(conn); err != nil {
+ if err := s.readRemoteInput(conn); err != nil {
log.Println(err)
}
}
@@ -110,3 +110,24 @@ func (s *Server) initRemoteConn(name string) (net.Conn, error) {
return conn, nil
}
+
+func (s *Server) readRemoteInput(conn net.Conn) error {
+ for {
+ payload, err := core.Decode(conn)
+ if err != nil {
+ return err
+ }
+ if err := s.handleRemotePayload(conn, payload); err != nil {
+ log.Print("handler error: ", err)
+ }
+ }
+}
+
+func (s *Server) handleRemotePayload(sender net.Conn, payload any) error {
+ switch v := payload.(type) {
+ case core.Message:
+ return s.handleMessage(sender, v)
+ default:
+ return core.ErrUnexpectedPayloadType
+ }
+}
diff --git a/server/server.go b/server/server.go
index 7573968..ef2bb5f 100644
--- a/server/server.go
+++ b/server/server.go
@@ -12,10 +12,12 @@ import (
type Server struct {
listenAddr string
name string
- users map[string]User
+ users map[string]User // TODO: Use full address instead of just name
servers map[string]RemoteServer
+ channels map[string]*Channel
usersMu sync.RWMutex
serversMu sync.RWMutex
+ channelsMu sync.RWMutex
}
func NewServer(listenAddr string, name string) *Server {
@@ -24,6 +26,7 @@ func NewServer(listenAddr string, name string) *Server {
name: name,
users: make(map[string]User),
servers: make(map[string]RemoteServer),
+ channels: make(map[string]*Channel),
}
}
@@ -45,6 +48,13 @@ func (s *Server) Start() error {
}
}
+func (s *Server) AddChannel(name string) {
+ s.channelsMu.Lock()
+ s.channelsMu.Unlock()
+ s.channels[name] = NewChannel(name)
+ log.Println("created channel", name)
+}
+
func (s *Server) handleConn(conn net.Conn) {
defer conn.Close()
@@ -87,25 +97,3 @@ func (s *Server) performHandshake(conn net.Conn) (core.ConnType, error) {
return clientHs.ConnType, nil
}
-
-func (s *Server) readInput(conn net.Conn) error {
- for {
- payload, err := core.Decode(conn)
- if err != nil {
- return err
- }
- if err := s.handlePayload(payload); err != nil {
- log.Print("handler error: ", err)
- }
- }
-}
-
-func (s *Server) handlePayload(payload any) error {
- switch v := payload.(type) {
- case core.Message:
- return s.handleMessage(v)
-
- default:
- return core.ErrUnexpectedPayloadType
- }
-}
diff --git a/server/user.go b/server/user.go
index 5d2731c..4f78b6a 100644
--- a/server/user.go
+++ b/server/user.go
@@ -8,22 +8,27 @@ import (
)
type User struct {
- Name string
- Conns map[net.Conn]struct{}
+ Name string
+ Conns map[net.Conn]struct{}
+ Channels map[string]*Channel
}
func NewUser(conn net.Conn, name string) User {
u := User{
- Name: name,
- Conns: make(map[net.Conn]struct{}),
+ Name: name,
+ Conns: make(map[net.Conn]struct{}),
+ Channels: make(map[string]*Channel),
}
u.Conns[conn] = struct{}{}
return u
}
-func (u *User) Send(payload core.Wrapper) error {
+func (u *User) Send(senderConn net.Conn, payload core.Wrapper) error {
for c := range u.Conns {
+ if c == senderConn {
+ continue
+ }
if err := core.Send(c, payload); err != nil {
return err
}
@@ -62,7 +67,7 @@ func (s *Server) handleUserConn(conn net.Conn) {
s.usersMu.Unlock()
}()
- if err := s.readInput(conn); err != nil {
+ if err := s.readUserInput(&user, conn); err != nil {
log.Println(err)
}
}
@@ -94,3 +99,55 @@ func (s *Server) performUserAuth(conn net.Conn) (string, error) {
return clientAuth.Name, nil
}
+
+func (s *Server) readUserInput(user *User, conn net.Conn) error {
+ for {
+ payload, err := core.Decode(conn)
+ if err != nil {
+ return err
+ }
+ if err := s.handleUserPayload(user, conn, payload); err != nil {
+ log.Print("handler error: ", err)
+ }
+ }
+}
+
+func (s *Server) handleUserPayload(user *User, sender net.Conn, payload any) error {
+ switch v := payload.(type) {
+ case core.Message:
+ return s.handleMessage(sender, v)
+ case core.Usermode:
+ return s.handleUsermode(user, sender, v)
+ default:
+ return core.ErrUnexpectedPayloadType
+ }
+}
+
+func (s *Server) handleUsermode(user *User, conn net.Conn, mode core.Usermode) error {
+ addr, err := core.ReadAddr(mode.UserAddr)
+ if err != nil {
+ return err
+ }
+
+ if user.Name != addr.Channel {
+ log.Println("unauthorized")
+ return user.Send(conn, core.Error{core.ErrorUnauthorized})
+ }
+
+ s.channelsMu.RLock()
+ channel, ok := s.channels[mode.ChannelName]
+ if !ok {
+ log.Println("not found", addr.Channel)
+ return user.Send(conn, core.Error{core.ErrorNotFound})
+ }
+ s.channelsMu.RUnlock()
+
+ switch mode.Mode {
+ case core.UsermodeNone:
+ channel.Remove(user)
+ case core.UsermodeInChannel:
+ channel.Add(user)
+ }
+
+ return nil
+}
diff --git a/tools/run-two-clients.sh b/tools/run-two-clients.sh
new file mode 100755
index 0000000..884a88a
--- /dev/null
+++ b/tools/run-two-clients.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+tmux \
+ new-session "go run cmd/daemon/main.go; read" \; \
+ split-window "sleep 0.5; go run cmd/client/main.go -u user1; read" \; \
+ split-window "sleep 0.5; go run cmd/client/main.go -u user1; read" \; \
+ split-window "sleep 0.5; go run cmd/client/main.go -u user2; read" \; \
+ select-layout tiled; \ No newline at end of file