diff options
| author | bt <bt@rctt.net> | 2026-04-19 21:32:53 +0200 |
|---|---|---|
| committer | bt <bt@rctt.net> | 2026-05-03 17:52:37 +0200 |
| commit | eec10d41af62fb9a93cd5fd79dcf94616701cc2a (patch) | |
| tree | d72068dcc4cb1aa43c2e0a2fae8ff094d41ed9c6 | |
| parent | c00e7dd589921e6be45918d4cd589e52e2b77036 (diff) | |
| download | solec-eec10d41af62fb9a93cd5fd79dcf94616701cc2a.tar.gz solec-eec10d41af62fb9a93cd5fd79dcf94616701cc2a.zip | |
[common] Basic group channels supportv0.3.0
| -rw-r--r-- | client/client.go | 20 | ||||
| -rw-r--r-- | cmd/client/main.go | 16 | ||||
| -rw-r--r-- | cmd/daemon/main.go | 1 | ||||
| -rw-r--r-- | core/data.go | 51 | ||||
| -rw-r--r-- | core/payload.go | 35 | ||||
| -rw-r--r-- | docs/rfc.md | 24 | ||||
| -rw-r--r-- | docs/rfc.txt | 35 | ||||
| -rw-r--r-- | server/channel.go | 48 | ||||
| -rw-r--r-- | server/message.go | 40 | ||||
| -rw-r--r-- | server/remote.go | 23 | ||||
| -rw-r--r-- | server/server.go | 34 | ||||
| -rw-r--r-- | server/user.go | 69 | ||||
| -rwxr-xr-x | tools/run-two-clients.sh | 8 |
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 |
