package server import ( "bytes" "errors" "fmt" "github.com/google/uuid" "github.com/mkaminski/goaim/oscar" "io" ) const ( ErrorCodeInvalidSnac uint16 = 0x01 ErrorCodeRateToHost uint16 = 0x02 ErrorCodeRateToClient uint16 = 0x03 ErrorCodeNotLoggedOn uint16 = 0x04 ErrorCodeServiceUnavailable uint16 = 0x05 ErrorCodeServiceNotDefined uint16 = 0x06 ErrorCodeObsoleteSnac uint16 = 0x07 ErrorCodeNotSupportedByHost uint16 = 0x08 ErrorCodeNotSupportedByClient uint16 = 0x09 ErrorCodeRefusedByClient uint16 = 0x0A ErrorCodeReplyTooBig uint16 = 0x0B ErrorCodeResponsesLost uint16 = 0x0C ErrorCodeRequestDenied uint16 = 0x0D ErrorCodeBustedSnacPayload uint16 = 0x0E ErrorCodeInsufficientRights uint16 = 0x0F ErrorCodeInLocalPermitDeny uint16 = 0x10 ErrorCodeTooEvilSender uint16 = 0x11 ErrorCodeTooEvilReceiver uint16 = 0x12 ErrorCodeUserTempUnavail uint16 = 0x13 ErrorCodeNoMatch uint16 = 0x14 ErrorCodeListOverflow uint16 = 0x15 ErrorCodeRequestAmbigous uint16 = 0x16 ErrorCodeQueueFull uint16 = 0x17 ErrorCodeNotWhileOnAol uint16 = 0x18 ErrorCodeQueryFail uint16 = 0x19 ErrorCodeTimeout uint16 = 0x1A ErrorCodeErrorText uint16 = 0x1B ErrorCodeGeneralFailure uint16 = 0x1C ErrorCodeProgress uint16 = 0x1D ErrorCodeInFreeArea uint16 = 0x1E ErrorCodeRestrictedByPc uint16 = 0x1F ErrorCodeRemoteRestrictedByPc uint16 = 0x20 ) const ( ErrorTagsFailUrl = 0x04 ErrorTagsErrorSubcode = 0x08 ErrorTagsErrorText = 0x1B ErrorTagsErrorInfoClsid = 0x29 ErrorTagsErrorInfoData = 0x2A ) var ( CapChat, _ = uuid.MustParse("748F2420-6287-11D1-8222-444553540000").MarshalBinary() ) var ( ErrUnsupportedFoodGroup = errors.New("unimplemented food group, your client version may be unsupported") ErrUnsupportedSubGroup = errors.New("unimplemented subgroup, your client version may be unsupported") ) type Config struct { BOSPort int `envconfig:"BOS_PORT" default:"5191"` ChatPort int `envconfig:"CHAT_PORT" default:"5192"` DBPath string `envconfig:"DB_PATH" required:"true"` DisableAuth bool `envconfig:"DISABLE_AUTH" default:"false"` FailFast bool `envconfig:"FAIL_FAST" default:"false"` OSCARHost string `envconfig:"OSCAR_HOST" required:"true"` OSCARPort int `envconfig:"OSCAR_PORT" default:"5190"` } func Address(host string, port int) string { return fmt.Sprintf("%s:%d", host, port) } func SendAndReceiveSignonFrame(rw io.ReadWriter, sequence *uint32) (oscar.FlapSignonFrame, error) { flapFrameOut := oscar.FlapFrame{ StartMarker: 42, FrameType: 1, Sequence: uint16(*sequence), PayloadLength: 4, // size of FlapSignonFrame } if err := oscar.Marshal(flapFrameOut, rw); err != nil { return oscar.FlapSignonFrame{}, err } flapSignonFrameOut := oscar.FlapSignonFrame{ FlapVersion: 1, } if err := oscar.Marshal(flapSignonFrameOut, rw); err != nil { return oscar.FlapSignonFrame{}, err } fmt.Printf("SendAndReceiveSignonFrame read FLAP: %+v\n", flapFrameOut) // receive flapFrameIn := oscar.FlapFrame{} if err := oscar.Unmarshal(&flapFrameIn, rw); err != nil { return oscar.FlapSignonFrame{}, err } b := make([]byte, flapFrameIn.PayloadLength) if _, err := rw.Read(b); err != nil { return oscar.FlapSignonFrame{}, err } flapSignonFrameIn := oscar.FlapSignonFrame{} if err := oscar.Unmarshal(&flapSignonFrameIn, bytes.NewBuffer(b)); err != nil { return oscar.FlapSignonFrame{}, err } fmt.Printf("SendAndReceiveSignonFrame write FLAP: %+v\n", flapSignonFrameIn) *sequence++ return flapSignonFrameIn, nil } func VerifyLogin(sm SessionManager, rw io.ReadWriter) (*Session, uint32, error) { seq := uint32(100) fmt.Println("VerifyLogin...") flap, err := SendAndReceiveSignonFrame(rw, &seq) if err != nil { return nil, 0, err } var ok bool ID, ok := flap.GetSlice(oscar.OServiceTLVTagsLoginCookie) if !ok { return nil, 0, errors.New("unable to get session ID from payload") } sess, ok := sm.Retrieve(string(ID)) if !ok { return nil, 0, fmt.Errorf("unable to find session by ID %s", ID) } return sess, seq, nil } func VerifyChatLogin(rw io.ReadWriter) (*ChatCookie, uint32, error) { seq := uint32(100) fmt.Println("VerifyChatLogin...") flap, err := SendAndReceiveSignonFrame(rw, &seq) if err != nil { return nil, 0, err } var ok bool buf, ok := flap.GetSlice(oscar.OServiceTLVTagsLoginCookie) if !ok { return nil, 0, errors.New("unable to get session ID from payload") } cookie := ChatCookie{} err = oscar.Unmarshal(&cookie, bytes.NewBuffer(buf)) return &cookie, seq, err } type IncomingMessage struct { flap oscar.FlapFrame snac oscar.SnacFrame buf io.Reader } type XMessage struct { snacFrame oscar.SnacFrame snacOut any } const ( FlapFrameSignon uint8 = 0x01 FlapFrameData = 0x02 FlapFrameError = 0x03 FlapFrameSignoff = 0x04 FlapFrameKeepAlive = 0x05 ) func sendInvalidSNACErr(snac oscar.SnacFrame, w io.Writer, sequence *uint32) error { snacFrameOut := oscar.SnacFrame{ FoodGroup: snac.FoodGroup, SubGroup: 0x01, // error subgroup for all SNACs } snacPayloadOut := oscar.SnacError{ Code: ErrorCodeInvalidSnac, } return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w) } func readIncomingRequests(rw io.Reader, msgCh chan IncomingMessage, errCh chan error) { defer close(msgCh) defer close(errCh) for { flap := oscar.FlapFrame{} if err := oscar.Unmarshal(&flap, rw); err != nil { errCh <- err return } switch flap.FrameType { case FlapFrameSignon: errCh <- errors.New("shouldn't get FlapFrameSignon") return case FlapFrameData: b := make([]byte, flap.PayloadLength) if _, err := rw.Read(b); err != nil { errCh <- err return } snac := oscar.SnacFrame{} buf := bytes.NewBuffer(b) if err := oscar.Unmarshal(&snac, buf); err != nil { errCh <- err return } msgCh <- IncomingMessage{ flap: flap, snac: snac, buf: buf, } case FlapFrameError: errCh <- fmt.Errorf("got FlapFrameError: %v", flap) return case FlapFrameSignoff: errCh <- ErrSignedOff return case FlapFrameKeepAlive: fmt.Println("keepalive heartbeat") default: errCh <- fmt.Errorf("unknown frame type: %v", flap) return } } } func Signout(sess *Session, sm SessionManager, fm *FeedbagStore) { if err := BroadcastDeparture(sess, sm, fm); err != nil { fmt.Printf("error notifying departure: %s", err.Error()) } sm.Remove(sess) } func ReadBos(cfg Config, sess *Session, seq uint32, sm SessionManager, fm *FeedbagStore, cr *ChatRegistry, rwc io.ReadWriter, room ChatRoom, router Router) error { if err := router.WriteOServiceHostOnline(rwc, &seq); err != nil { return err } // buffered so that the go routine has room to exit msgCh := make(chan IncomingMessage, 1) errCh := make(chan error, 1) go readIncomingRequests(rwc, msgCh, errCh) for { select { case m := <-msgCh: if err := router.routeIncomingRequests(cfg, sm, sess, fm, cr, rwc, &seq, m.snac, m.buf, room); err != nil { switch { case errors.Is(err, ErrUnsupportedSubGroup) || errors.Is(err, ErrUnsupportedFoodGroup): if err := sendInvalidSNACErr(m.snac, rwc, &seq); err != nil { return err } msg := fmt.Sprintf("unimplemented SNAC: %+v", m.snac) if cfg.FailFast { panic(msg) } fmt.Println(msg) default: return err } } case m := <-sess.RecvMessage(): if err := writeOutSNAC(oscar.SnacFrame{}, m.snacFrame, m.snacOut, &seq, rwc); err != nil { return err } case err := <-errCh: return err } } } func NewRouter() Router { return Router{ BuddyRouter: NewBuddyRouter(), ChatNavRouter: NewChatNavRouter(), ChatRouter: NewChatRouter(), FeedbagRouter: NewFeedbagRouter(), ICBMRouter: NewICBMRouter(), LocateRouter: NewLocateRouter(), OServiceRouter: NewOServiceRouter(), } } func NewRouterForChat() Router { r := NewRouter() r.OServiceRouter = NewOServiceRouterForChat() return r } type Router struct { BuddyRouter ChatNavRouter ChatRouter FeedbagRouter ICBMRouter LocateRouter OServiceRouter } func (rt *Router) routeIncomingRequests(cfg Config, sm SessionManager, sess *Session, fm *FeedbagStore, cr *ChatRegistry, rw io.ReadWriter, sequence *uint32, snac oscar.SnacFrame, buf io.Reader, room ChatRoom) error { switch snac.FoodGroup { case oscar.OSERVICE: return rt.RouteOService(cfg, cr, sm, fm, sess, room, snac, buf, rw, sequence) case oscar.LOCATE: return rt.RouteLocate(sess, sm, fm, snac, buf, rw, sequence) case oscar.BUDDY: return rt.RouteBuddy(snac, buf, rw, sequence) case oscar.ICBM: return rt.RouteICBM(sm, fm, sess, snac, buf, rw, sequence) case oscar.CHAT_NAV: return rt.RouteChatNav(sess, cr, snac, buf, rw, sequence) case oscar.FEEDBAG: return rt.RouteFeedbag(sm, sess, fm, snac, buf, rw, sequence) case oscar.BUCP: return routeBUCP() case oscar.CHAT: return rt.RouteChat(sess, sm, snac, buf, rw, sequence) default: return ErrUnsupportedFoodGroup } } func writeOutSNAC(originsnac oscar.SnacFrame, snacFrame oscar.SnacFrame, snacOut any, sequence *uint32, w io.Writer) error { if originsnac.RequestID != 0 { snacFrame.RequestID = originsnac.RequestID } snacBuf := &bytes.Buffer{} if err := oscar.Marshal(snacFrame, snacBuf); err != nil { return err } if err := oscar.Marshal(snacOut, snacBuf); err != nil { return err } flap := oscar.FlapFrame{ StartMarker: 42, FrameType: 2, Sequence: uint16(*sequence), PayloadLength: uint16(snacBuf.Len()), } fmt.Printf(" write FLAP: %+v\n", flap) if err := oscar.Marshal(flap, w); err != nil { return err } fmt.Printf(" write SNAC: %+v\n", snacOut) expectLen := snacBuf.Len() c, err := w.Write(snacBuf.Bytes()) if err != nil { return err } if c != expectLen { panic("did not write the expected # of bytes") } *sequence++ return nil }