diff options
Diffstat (limited to 'internal/server')
| -rw-r--r-- | internal/server/client.go | 58 | ||||
| -rw-r--r-- | internal/server/conn.go | 67 | ||||
| -rw-r--r-- | internal/server/display.go | 83 | ||||
| -rw-r--r-- | internal/server/escapes.go | 49 | ||||
| -rw-r--r-- | internal/server/server.go | 70 |
5 files changed, 327 insertions, 0 deletions
diff --git a/internal/server/client.go b/internal/server/client.go new file mode 100644 index 0000000..dbea5a9 --- /dev/null +++ b/internal/server/client.go @@ -0,0 +1,58 @@ +package server + +import ( + "context" + "io" + "log/slog" + + "go.neonxp.ru/qchat/internal/chat" + "golang.org/x/term" +) + +func (s *Server) serveClient(ctx context.Context, rw io.ReadWriteCloser, user *chat.User) { + t := term.NewTerminal(rw, "[] ") + + go func() { + // defer s.bus.Unsubscribe(evCh) + for { + select { + case <-ctx.Done(): + return + default: + } + if user.CurrentChan != nil { + t.SetPrompt("[" + user.CurrentChan.Name + "] ") + } + line, err := t.ReadLine() + if err != nil { + s.chat.RemoveUser(user) + + if err != io.EOF { + slog.Error("failed read line", slog.Any("err", err)) + } + + return + } + if len(line) == 0 { + continue + } + + s.chat.Input(ctx, user, line) + } + }() + + for message := range user.Events { + switch message := message.(type) { + case chat.Message: + displayMessage(t, message, user) + case chat.SelfMessage: + displaySelfMessage(t, message) + case chat.SystemMessage: + displaySystemMessage(t, message) + case chat.UserJoined: + displayUserJoined(t, message) + case chat.UserLeft: + displayUserLeft(t, message) + } + } +} diff --git a/internal/server/conn.go b/internal/server/conn.go new file mode 100644 index 0000000..1069b0b --- /dev/null +++ b/internal/server/conn.go @@ -0,0 +1,67 @@ +package server + +import ( + "context" + "fmt" + "log/slog" + "net" + "sync" + + "golang.org/x/crypto/ssh" +) + +func (s *Server) serveConn(ctx context.Context, nConn net.Conn, config *ssh.ServerConfig) error { + conn, chans, reqs, err := ssh.NewServerConn(nConn, config) + if err != nil { + return fmt.Errorf("failed to handshake: %w", err) + } + slog.Info("user connected", slog.Any("user", conn.User()), slog.String("ip", conn.RemoteAddr().String())) + + var wg sync.WaitGroup + defer wg.Wait() + + wg.Go(func() { + ssh.DiscardRequests(reqs) + }) + + for newChannel := range chans { + if newChannel.ChannelType() != "session" { + newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") + continue + } + channel, requests, err := newChannel.Accept() + if err != nil { + return fmt.Errorf("could not accept channel: %w", err) + } + + wg.Go(func() { + for req := range requests { + switch req.Type { + case "pty-req": + req.Reply(true, nil) + case "shell": + req.Reply(true, nil) + default: + req.Reply(false, nil) + } + slog.Debug( + "req", + slog.String("type", req.Type), + slog.Bool("want-reply", req.WantReply), + slog.String("payload", string(req.Payload)), + ) + } + }) + + wg.Go(func() { + identify := conn.Permissions.ExtraData["identify"].(string) + user := s.chat.NewUser(conn.User(), identify) + slog.Info("joined", slog.String("user", user.NUsername())) + s.serveClient(ctx, channel, user) + slog.Info("disconnected", slog.String("user", user.NUsername())) + conn.Close() + }) + } + + return nil +} diff --git a/internal/server/display.go b/internal/server/display.go new file mode 100644 index 0000000..90d481a --- /dev/null +++ b/internal/server/display.go @@ -0,0 +1,83 @@ +package server + +import ( + "fmt" + "io" + "regexp" + "strings" + + "go.neonxp.ru/qchat/internal/chat" +) + +var replacements = []replacement{ + { + From: *regexp.MustCompile(`\*(.+?)\*`), + To: fmt.Sprintf("%s$1%s", escapeCodes[Bold], escapeCodes[Reset]), + }, + { + From: *regexp.MustCompile(`_(.+?)_`), + To: fmt.Sprintf("%s$1%s", escapeCodes[Italic], escapeCodes[Reset]), + }, +} + +func displayMessage(t io.Writer, message chat.Message, user *chat.User) { + msg := message.Message + mentionFrom := "@" + user.Username + mentionTo := string(escapeCodes[Blink]) + string(escapeCodes[Underline]) + "@" + user.Username + string(escapeCodes[Reset]) + if strings.Contains(msg, mentionFrom) { + msg = strings.ReplaceAll(msg, mentionFrom, mentionTo) + msg = "\x07\a" + msg + } + + for _, r := range replacements { + msg = r.From.ReplaceAllString(msg, r.To) + } + + fmt.Fprintf(t, "%s%s%s\t[%s]%s: %s\n", + escapeCodes[Blue], + message.User.NUsername(), + escapeCodes[Green], + message.Time.Format("15:04:05"), + escapeCodes[Reset], + msg, + ) +} + +func displaySystemMessage(t io.Writer, message chat.SystemMessage) { + fmt.Fprintf(t, "%s* %s %s\n", + escapeCodes[Green], + message.Message, + escapeCodes[Reset], + ) +} +func displaySelfMessage(t io.Writer, message chat.SelfMessage) { + fmt.Fprintf(t, "%s* %s %s%s\n", + escapeCodes[Blue], + message.User.NUsername(), + message.Message, + escapeCodes[Reset], + ) +} + +func displayUserJoined(t io.Writer, presence chat.UserJoined) { + fmt.Fprintf(t, "%s* %s joined to %s chan%s\n", + escapeCodes[Green], + presence.User.NUsername(), + presence.Chan.Name, + escapeCodes[Reset], + ) +} + +func displayUserLeft(t io.Writer, presence chat.UserLeft) { + fmt.Fprintf(t, "%s* %s left %s chan%s\n", + escapeCodes[Red], + presence.User.NUsername(), + presence.Chan.Name, + escapeCodes[Reset], + ) +} + +type replacement struct { + From regexp.Regexp + To string +} diff --git a/internal/server/escapes.go b/internal/server/escapes.go new file mode 100644 index 0000000..f495345 --- /dev/null +++ b/internal/server/escapes.go @@ -0,0 +1,49 @@ +package server + +const ( + keyEscape = 27 +) + +var escapeCodes = map[color][]byte{ + Black: {keyEscape, '[', '3', '0', 'm'}, + Red: {keyEscape, '[', '3', '1', 'm'}, + Green: {keyEscape, '[', '3', '2', 'm'}, + Yellow: {keyEscape, '[', '3', '3', 'm'}, + Blue: {keyEscape, '[', '3', '4', 'm'}, + Magenta: {keyEscape, '[', '3', '5', 'm'}, + Cyan: {keyEscape, '[', '3', '6', 'm'}, + White: {keyEscape, '[', '3', '7', 'm'}, + Reset: {keyEscape, '[', '0', 'm'}, + Bold: {keyEscape, '[', '1', 'm'}, + Faint: {keyEscape, '[', '2', 'm'}, + Italic: {keyEscape, '[', '3', 'm'}, + Underline: {keyEscape, '[', '4', 'm'}, + Blink: {keyEscape, '[', '5', 'm'}, + Rapid: {keyEscape, '[', '6', 'm'}, + Reverse: {keyEscape, '[', '7', 'm'}, + Conceal: {keyEscape, '[', '8', 'm'}, + Strike: {keyEscape, '[', '9', 'm'}, +} + +type color int + +const ( + Black color = iota + Red + Green + Yellow + Blue + Magenta + Cyan + White + Reset + Bold + Faint + Italic + Underline + Blink + Rapid + Reverse + Conceal + Strike +) diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..1c28cd5 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,70 @@ +package server + +import ( + "context" + "encoding/hex" + "fmt" + "log/slog" + "net" + "sync" + + "go.neonxp.ru/qchat/internal/chat" + "go.neonxp.ru/qchat/internal/config" + "golang.org/x/crypto/ssh" +) + +type Server struct { + cfg *config.Config + chat *chat.Chat +} + +func New(cfg *config.Config, ch *chat.Chat) *Server { + return &Server{ + cfg: cfg, + chat: ch, + } +} + +func (s *Server) Run(ctx context.Context) error { + config := &ssh.ServerConfig{ + PublicKeyCallback: func(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) { + return &ssh.Permissions{ + ExtraData: map[any]any{ + "identify": hex.EncodeToString(pubKey.Marshal()), + }, + }, nil + }, + } + + private, err := ssh.ParsePrivateKey([]byte(s.cfg.Server.PrivateKey)) + if err != nil { + return fmt.Errorf("failed to parse private key: %w", err) + } + config.AddHostKey(private) + listener, err := net.Listen("tcp", s.cfg.Server.Addr) + if err != nil { + return fmt.Errorf("failed to parse private key: %w", err) + } + go func() { + <-ctx.Done() + listener.Close() + }() + + wg := sync.WaitGroup{} + defer wg.Wait() + + slog.InfoContext(ctx, "started server at", slog.String("addr", s.cfg.Server.Addr)) + + for { + nConn, err := listener.Accept() + if err != nil { + slog.ErrorContext(ctx, "failed to accept incoming connection", slog.Any("err", err)) + continue + } + wg.Go(func() { + if err := s.serveConn(ctx, nConn, config); err != nil { + slog.ErrorContext(ctx, "connection error", slog.Any("err", err)) + } + }) + } +} |
