| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 |
- package icq_legacy
- import (
- "context"
- "encoding/binary"
- "fmt"
- "log/slog"
- "net"
- )
- // V1Handler handles ICQ V1 protocol packets.
- // V1 uses the same wire format as V2 for most commands, so this embeds
- // V2Handler. The key difference is that V1's CMD_GET_DEPS (0x03F2) uses
- // the standard V2 10-byte header (VERSION+CMD+SEQ+UIN), whereas V2 clients
- // that send 0x03F2 use a V3-style 12-byte header. V1Handler overrides
- // Handle to intercept 0x03F2 and parse it with the correct layout.
- type V1Handler struct {
- *V2Handler
- }
- // NewV1Handler creates a new V1 protocol handler.
- func NewV1Handler(
- sessions *LegacySessionManager,
- service LegacyService,
- sender PacketSender,
- logger *slog.Logger,
- ) *V1Handler {
- v1pb := NewV1PacketBuilder()
- return &V1Handler{
- V2Handler: &V2Handler{
- BaseHandler: BaseHandler{
- sessions: sessions,
- service: service,
- sender: sender,
- logger: logger,
- },
- packetBuilder: v1pb,
- },
- }
- }
- // Handle processes a V1 protocol packet.
- // Intercepts CMD_GET_DEPS (0x03F2) to parse with V1's 10-byte header layout,
- // then delegates everything else to V2Handler.
- func (h *V1Handler) Handle(session *LegacySession, addr *net.UDPAddr, packet []byte) error {
- // Peek at the command field (bytes 2-4) to check for 0x03F2
- if len(packet) >= 4 {
- cmd := binary.LittleEndian.Uint16(packet[2:4])
- if cmd == ICQLegacyCmdGetDeps {
- return h.handleV1Login(session, addr, packet)
- }
- }
- // Everything else uses the same format as V2
- return h.V2Handler.Handle(session, addr, packet)
- }
- // handleV1Login processes the V1 login packet (0x03F2).
- // V1 clients use 0x03F2 as their actual login command (not a pre-auth step).
- // The packet uses the standard 10-byte header: VERSION(2) + CMD(2) + SEQ(2) + UIN(4)
- // Data payload: PWD_LEN(2) + PASSWORD(variable, null-terminated) + STATUS(4)
- //
- // Unlike V2 clients which send 0x03E8 (CMD_LOGIN) directly, and V3+ clients
- // which use 0x03F2 as a pre-auth step before 0x03E8, V1 clients use 0x03F2
- // as their only login command and expect SRV_HELLO (0x005A) in response.
- func (h *V1Handler) handleV1Login(session *LegacySession, addr *net.UDPAddr, packet []byte) error {
- ctx := context.Background()
- // V1 header: VERSION(2) + CMD(2) + SEQ(2) + UIN(4) = 10 bytes
- if len(packet) < 12 { // 10-byte header + at least 2 bytes for pwd_len
- h.logger.Debug("V1 login packet too short", "len", len(packet))
- return nil
- }
- seqNum := binary.LittleEndian.Uint16(packet[4:6])
- uin := binary.LittleEndian.Uint32(packet[6:10])
- data := packet[10:]
- h.logger.Debug("V1 login packet (0x03F2)",
- "addr", addr.String(),
- "seq", seqNum,
- "uin", uin,
- "data_len", len(data),
- "data_hex", fmt.Sprintf("%X", data),
- )
- // Parse password from data: PWD_LEN(2) + PASSWORD
- if len(data) < 2 {
- return nil
- }
- pwdLen := binary.LittleEndian.Uint16(data[0:2])
- offset := 2
- if pwdLen == 0 || pwdLen > 20 || offset+int(pwdLen) > len(data) {
- h.logger.Debug("invalid password in V1 login", "pwd_len", pwdLen)
- return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seqNum, ICQLegacyVersionV1))
- }
- password := string(data[offset : offset+int(pwdLen)])
- if len(password) > 0 && password[len(password)-1] == 0 {
- password = password[:len(password)-1]
- }
- offset += int(pwdLen)
- // Parse status (4 bytes) if present
- var status uint32
- if offset+4 <= len(data) {
- status = binary.LittleEndian.Uint32(data[offset : offset+4])
- }
- h.logger.Info("V1 login attempt",
- "uin", uin,
- "password_len", len(password),
- "status", fmt.Sprintf("0x%08X", status),
- )
- // Authenticate using service layer
- authReq := AuthRequest{
- UIN: uin,
- Password: password,
- Status: status,
- Version: ICQLegacyVersionV1,
- }
- authResult, err := h.service.AuthenticateUser(ctx, authReq)
- if err != nil || !authResult.Success {
- h.logger.Info("V1 login failed - invalid credentials", "uin", uin)
- return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seqNum, ICQLegacyVersionV1))
- }
- // Create session
- newSession, err := h.sessions.CreateSession(uin, addr, ICQLegacyVersionV1, authResult.oscarSession)
- if err != nil {
- h.logger.Error("failed to create V1 session", "err", err, "uin", uin)
- return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seqNum, ICQLegacyVersionV1))
- }
- newSession.SetStatus(status)
- newSession.Password = password
- // Send ACK
- ackPkt := h.packetBuilder.BuildAck(seqNum, ICQLegacyVersionV1)
- if err := h.sender.SendToSession(newSession, ackPkt); err != nil {
- return err
- }
- // Send SRV_HELLO (0x005A) login reply — same as V2 gets for CMD_LOGIN
- loginReplyPkt := h.packetBuilder.BuildLoginReply(newSession, seqNum)
- if err := h.sender.SendToSession(newSession, loginReplyPkt); err != nil {
- return err
- }
- h.logger.Info("V1 login successful",
- "uin", uin,
- "session_id", newSession.SessionID,
- )
- // Notify contacts that user is online
- if err := h.service.NotifyStatusChange(ctx, uin, status); err != nil {
- h.logger.Debug("failed to notify status change", "err", err)
- }
- return nil
- }
- // SetSender sets the packet sender (for circular dependency resolution).
- func (h *V1Handler) SetSender(sender PacketSender) {
- h.V2Handler.SetSender(sender)
- }
- // SetDispatcher sets the message dispatcher for cross-protocol message routing.
- func (h *V1Handler) SetDispatcher(dispatcher MessageDispatcher) {
- h.V2Handler.SetDispatcher(dispatcher)
- }
|