| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302 |
- package icq_legacy
- import (
- "context"
- "errors"
- "fmt"
- "log/slog"
- "net"
- "sync"
- "time"
- "github.com/mk6i/open-oscar-server/config"
- )
- const (
- // MaxPacketSize is the maximum UDP packet size we'll accept
- MaxPacketSize = 8192
- // ReadBufferSize is the UDP socket read buffer size
- ReadBufferSize = 65536
- )
- var (
- ErrServerClosed = errors.New("server closed")
- ErrUnsupportedProto = errors.New("unsupported protocol version")
- )
- // LegacyServer handles legacy ICQ protocol connections over UDP
- type LegacyServer struct {
- conn *net.UDPConn
- config config.ICQLegacyConfig
- sessions *LegacySessionManager
- dispatcher *ProtocolDispatcher
- logger *slog.Logger
- stopChan chan struct{}
- wg sync.WaitGroup
- mu sync.RWMutex
- running bool
- }
- // NewLegacyServer creates a new legacy ICQ server
- func NewLegacyServer(
- cfg config.ICQLegacyConfig,
- sessions *LegacySessionManager,
- dispatcher *ProtocolDispatcher,
- logger *slog.Logger,
- ) *LegacyServer {
- return &LegacyServer{
- config: cfg,
- sessions: sessions,
- dispatcher: dispatcher,
- logger: logger,
- stopChan: make(chan struct{}),
- }
- }
- // Start begins listening for legacy ICQ connections
- func (s *LegacyServer) Start(ctx context.Context) error {
- if !s.config.Enabled {
- s.logger.Info("legacy ICQ server disabled")
- return nil
- }
- s.mu.Lock()
- if s.running {
- s.mu.Unlock()
- return errors.New("server already running")
- }
- s.running = true
- s.mu.Unlock()
- // Parse listen address
- addr, err := net.ResolveUDPAddr("udp4", s.config.UDPListener)
- if err != nil {
- return fmt.Errorf("invalid UDP listener address: %w", err)
- }
- // Create UDP socket
- conn, err := net.ListenUDP("udp4", addr)
- if err != nil {
- return fmt.Errorf("failed to listen on UDP: %w", err)
- }
- // Set socket options
- if err := conn.SetReadBuffer(ReadBufferSize); err != nil {
- s.logger.Warn("failed to set read buffer size", "err", err)
- }
- s.conn = conn
- s.logger.Info("legacy ICQ server started",
- "address", s.config.UDPListener,
- "versions", s.config.SupportedVersions,
- )
- // Start session cleanup routine
- s.wg.Add(1)
- go func() {
- defer s.wg.Done()
- s.sessions.StartCleanupRoutine(s.config.SessionTimeout/2, s.stopChan)
- }()
- // Start packet receive loop
- s.wg.Add(1)
- go func() {
- defer s.wg.Done()
- s.receiveLoop(ctx)
- }()
- return nil
- }
- // Stop gracefully shuts down the server
- func (s *LegacyServer) Stop() error {
- s.mu.Lock()
- if !s.running {
- s.mu.Unlock()
- return nil
- }
- s.running = false
- s.mu.Unlock()
- // Signal all goroutines to stop
- close(s.stopChan)
- // Close the UDP socket to unblock the receive loop
- if s.conn != nil {
- _ = s.conn.Close()
- }
- // Wait for all goroutines to finish
- s.wg.Wait()
- s.logger.Info("legacy ICQ server stopped")
- return nil
- }
- // receiveLoop handles incoming UDP packets
- func (s *LegacyServer) receiveLoop(ctx context.Context) {
- buf := make([]byte, MaxPacketSize)
- for {
- select {
- case <-ctx.Done():
- return
- case <-s.stopChan:
- return
- default:
- }
- // Set read deadline to allow periodic checking of stop signal
- _ = s.conn.SetReadDeadline(time.Now().Add(1 * time.Second))
- n, addr, err := s.conn.ReadFromUDP(buf)
- if err != nil {
- // Check if it's a timeout (expected)
- if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
- continue
- }
- // Check if server is stopping
- select {
- case <-s.stopChan:
- return
- default:
- }
- s.logger.Error("UDP read error", "err", err)
- continue
- }
- if n < 2 {
- s.logger.Debug("packet too short", "size", n, "addr", addr)
- continue
- }
- // Copy packet data to avoid buffer reuse issues
- packet := make([]byte, n)
- copy(packet, buf[:n])
- // Handle packet in goroutine to not block receive loop
- go s.handlePacket(addr, packet)
- }
- }
- // handlePacket processes a single incoming packet
- func (s *LegacyServer) handlePacket(addr *net.UDPAddr, packet []byte) {
- // Detect protocol version
- version, err := DetectProtocolVersion(packet)
- if err != nil {
- s.logger.Debug("failed to detect protocol version",
- "err", err,
- "addr", addr,
- )
- return
- }
- // Check if version is supported
- if !s.config.SupportsVersion(int(version)) {
- s.logger.Debug("unsupported protocol version",
- "version", version,
- "addr", addr,
- "size", len(packet),
- "hex", fmt.Sprintf("%X", packet),
- )
- return
- }
- // Get or create session
- session := s.sessions.GetSessionByAddr(addr)
- // Dispatch to appropriate handler
- if err := s.dispatcher.Dispatch(session, addr, packet); err != nil {
- s.logger.Debug("packet dispatch error",
- "err", err,
- "addr", addr,
- "version", version,
- )
- }
- }
- // SendPacket sends a packet to a specific address
- func (s *LegacyServer) SendPacket(addr *net.UDPAddr, packet []byte) error {
- s.mu.RLock()
- if !s.running || s.conn == nil {
- s.mu.RUnlock()
- return ErrServerClosed
- }
- conn := s.conn
- s.mu.RUnlock()
- s.logger.Debug("sending packet",
- "to", addr.String(),
- "size", len(packet),
- "hex", fmt.Sprintf("%X", packet),
- )
- _, err := conn.WriteToUDP(packet, addr)
- if err != nil {
- s.logger.Debug("failed to send packet",
- "err", err,
- "addr", addr,
- "size", len(packet),
- )
- return err
- }
- return nil
- }
- // SendToSession sends a packet to a session
- func (s *LegacyServer) SendToSession(session *LegacySession, packet []byte) error {
- if session == nil || session.Addr == nil {
- return errors.New("invalid session")
- }
- return s.SendPacket(session.Addr, packet)
- }
- // BroadcastToAll sends a packet to all connected sessions
- func (s *LegacyServer) BroadcastToAll(packet []byte) {
- sessions := s.sessions.GetAllSessions()
- for _, session := range sessions {
- if err := s.SendToSession(session, packet); err != nil {
- s.logger.Debug("broadcast send failed",
- "uin", session.UIN,
- "err", err,
- )
- }
- }
- }
- // IsRunning returns whether the server is currently running
- func (s *LegacyServer) IsRunning() bool {
- s.mu.RLock()
- defer s.mu.RUnlock()
- return s.running
- }
- // SessionCount returns the number of active sessions
- func (s *LegacyServer) SessionCount() int {
- return s.sessions.Count()
- }
- // ListenAndServe starts the server and blocks until it's stopped.
- // This method matches the interface used by other servers in the project.
- func (s *LegacyServer) ListenAndServe() error {
- ctx := context.Background()
- if err := s.Start(ctx); err != nil {
- return err
- }
- // Block until server is stopped
- <-s.stopChan
- return nil
- }
- // Shutdown gracefully shuts down the server.
- // This method matches the interface used by other servers in the project.
- func (s *LegacyServer) Shutdown(ctx context.Context) error {
- return s.Stop()
- }
|