mirror of
https://github.com/amigan/aim-oscar-server.git
synced 2024-10-18 05:33:32 -04:00
241 lines
6.7 KiB
Go
241 lines
6.7 KiB
Go
package main
|
|
|
|
import (
|
|
"aim-oscar/models"
|
|
"aim-oscar/oscar"
|
|
"aim-oscar/util"
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/uptrace/bun"
|
|
"github.com/uptrace/bun/dbfixture"
|
|
"github.com/uptrace/bun/dialect/sqlitedialect"
|
|
"github.com/uptrace/bun/driver/sqliteshim"
|
|
"github.com/uptrace/bun/extra/bundebug"
|
|
)
|
|
|
|
const (
|
|
SRV_HOST = "10.0.1.2"
|
|
SRV_PORT = "5190"
|
|
SRV_ADDRESS = SRV_HOST + ":" + SRV_PORT
|
|
)
|
|
|
|
var services map[uint16]Service
|
|
|
|
// Map username to session
|
|
var sessions map[string]*oscar.Session
|
|
|
|
func init() {
|
|
services = make(map[uint16]Service)
|
|
sessions = make(map[string]*oscar.Session)
|
|
}
|
|
|
|
func RegisterService(family uint16, service Service) {
|
|
services[family] = service
|
|
}
|
|
|
|
func main() {
|
|
// Set up the DB
|
|
sqldb, err := sql.Open(sqliteshim.ShimName, "file:aim.db")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
db := bun.NewDB(sqldb, sqlitedialect.New())
|
|
db.SetConnMaxIdleTime(15 * time.Second)
|
|
db.SetConnMaxLifetime(1 * time.Minute)
|
|
|
|
// Print all queries to stdout.
|
|
db.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true)))
|
|
|
|
// Register our DB models
|
|
db.RegisterModel((*models.User)(nil), (*models.Message)(nil))
|
|
|
|
// dev: load in fixtures to test against
|
|
fixture := dbfixture.New(db, dbfixture.WithRecreateTables())
|
|
err = fixture.Load(context.Background(), os.DirFS("models"), "fixtures.yml")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
listener, err := net.Listen("tcp", SRV_ADDRESS)
|
|
if err != nil {
|
|
fmt.Println("Error listening: ", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
defer listener.Close()
|
|
|
|
// Goroutine that listens for messages to deliver and tries to find a user socket to push them to
|
|
commCh := make(chan *models.Message, 1)
|
|
go func() {
|
|
for {
|
|
message, more := <-commCh
|
|
if !more {
|
|
log.Printf("message delivery routine shutting down")
|
|
return
|
|
}
|
|
|
|
log.Printf("got a message: %s", message)
|
|
if s, ok := sessions[message.To]; ok {
|
|
messageSnac := oscar.NewSNAC(4, 7)
|
|
messageSnac.Data.WriteUint64(message.MessageID)
|
|
messageSnac.Data.WriteUint16(1)
|
|
messageSnac.Data.WriteLPString(message.From)
|
|
messageSnac.Data.WriteUint16(0) // TODO: sender's warning level
|
|
|
|
tlvs := []*oscar.TLV{
|
|
oscar.NewTLV(1, util.Word(0x80)), // TODO: user class
|
|
oscar.NewTLV(6, util.Dword(0x0001|0x0100)), // TODO: user status
|
|
oscar.NewTLV(0xf, util.Dword(0)), // TODO: user idle time
|
|
oscar.NewTLV(3, util.Dword(0)), // TODO: user creation time
|
|
// oscar.NewTLV(4, []byte{}), // TODO: this TLV appears in automated responses like away messages
|
|
}
|
|
|
|
// Length of TLVs in fixed part
|
|
messageSnac.Data.WriteUint16(uint16(len(tlvs)))
|
|
|
|
// Write all of the TLVs to the SNAC
|
|
for _, tlv := range tlvs {
|
|
messageSnac.Data.WriteBinary(tlv)
|
|
}
|
|
|
|
frag := oscar.Buffer{}
|
|
frag.Write([]byte{5, 1, 0, 4, 1, 1, 1, 1}) // TODO: first fragment [id, version, len, len, (cap * len)... ]
|
|
frag.Write([]byte{1, 1}) // message text fragment start (this is a busted "TLV")
|
|
frag.Write(util.Word(uint16(len(message.Contents) + 4))) // length of TLV
|
|
frag.Write([]byte{0, 0, 0, 0}) // TODO: message charset number, message charset subset
|
|
frag.WriteString(message.Contents)
|
|
|
|
// Append the fragments
|
|
messageSnac.Data.Write(frag.Bytes())
|
|
|
|
messageFlap := oscar.NewFLAP(2)
|
|
messageFlap.Data.WriteBinary(messageSnac)
|
|
if err := s.Send(messageFlap); err != nil {
|
|
log.Panicf("could not deliver message %d: %s", message.MessageID, err.Error())
|
|
continue
|
|
} else {
|
|
log.Printf("sent message %d to user %s at %s", message.MessageID, message.To, s.RemoteAddr())
|
|
}
|
|
|
|
if err := message.MarkDelivered(context.Background(), db); err != nil {
|
|
log.Panicf("could not mark message %d as delivered: %s", message.MessageID, err.Error())
|
|
}
|
|
} else {
|
|
log.Printf("could not find session for user %s", message.To)
|
|
}
|
|
}
|
|
}()
|
|
|
|
handleCloseFn := func(ctx context.Context, session *oscar.Session) {
|
|
log.Printf("%v disconnected", session.RemoteAddr())
|
|
|
|
user := models.UserFromContext(ctx)
|
|
if user != nil {
|
|
user.Status = "offline"
|
|
if err := user.Update(ctx, db); err != nil {
|
|
log.Print(errors.Wrap(err, "could not set user as active"))
|
|
}
|
|
}
|
|
}
|
|
|
|
handleFn := func(ctx context.Context, flap *oscar.FLAP) context.Context {
|
|
session, err := oscar.SessionFromContext(ctx)
|
|
if err != nil {
|
|
util.PanicIfError(err)
|
|
}
|
|
|
|
if user := models.UserFromContext(ctx); user != nil {
|
|
fmt.Printf("%s (%v) ->\n%+v\n", user.Username, session.RemoteAddr(), flap)
|
|
user.LastActivityAt = time.Now()
|
|
ctx = models.NewContextWithUser(ctx, user)
|
|
sessions[user.Username] = session
|
|
} else {
|
|
fmt.Printf("%v ->\n%+v\n", session.RemoteAddr(), flap)
|
|
}
|
|
|
|
if flap.Header.Channel == 1 {
|
|
// Is this a hello?
|
|
if bytes.Equal(flap.Data.Bytes(), []byte{0, 0, 0, 1}) {
|
|
return ctx
|
|
}
|
|
|
|
user, err := AuthenticateFLAPCookie(ctx, db, flap)
|
|
if err != nil {
|
|
log.Printf("Could not authenticate cookie: %s", err)
|
|
return ctx
|
|
}
|
|
ctx = models.NewContextWithUser(ctx, user)
|
|
|
|
// Send available services
|
|
servicesSnac := oscar.NewSNAC(1, 3)
|
|
servicesSnac.Data.WriteUint16(0x1)
|
|
servicesSnac.Data.WriteUint16(0x4)
|
|
servicesFlap := oscar.NewFLAP(2)
|
|
servicesFlap.Data.WriteBinary(servicesSnac)
|
|
session.Send(servicesFlap)
|
|
|
|
return ctx
|
|
} else if flap.Header.Channel == 2 {
|
|
snac := &oscar.SNAC{}
|
|
err := snac.UnmarshalBinary(flap.Data.Bytes())
|
|
util.PanicIfError(err)
|
|
|
|
fmt.Printf("%+v\n", snac)
|
|
if tlvs, err := oscar.UnmarshalTLVs(snac.Data.Bytes()); err == nil {
|
|
for _, tlv := range tlvs {
|
|
fmt.Printf("%+v\n", tlv)
|
|
}
|
|
} else {
|
|
fmt.Printf("%s\n\n", util.PrettyBytes(snac.Data.Bytes()))
|
|
}
|
|
|
|
if service, ok := services[snac.Header.Family]; ok {
|
|
newCtx, err := service.HandleSNAC(ctx, db, snac, commCh)
|
|
util.PanicIfError(err)
|
|
return newCtx
|
|
}
|
|
} else if flap.Header.Channel == 4 {
|
|
session.Disconnect()
|
|
handleCloseFn(ctx, session)
|
|
}
|
|
|
|
return ctx
|
|
}
|
|
|
|
handler := oscar.NewHandler(handleFn, handleCloseFn)
|
|
|
|
RegisterService(0x17, &AuthorizationRegistrationService{})
|
|
RegisterService(0x01, &GenericServiceControls{})
|
|
RegisterService(0x04, &ICBM{})
|
|
|
|
exitChan := make(chan os.Signal, 1)
|
|
signal.Notify(exitChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGABRT)
|
|
go func() {
|
|
<-exitChan
|
|
close(commCh)
|
|
fmt.Println("Shutting down")
|
|
os.Exit(1)
|
|
}()
|
|
|
|
fmt.Println("OSCAR listening on " + SRV_ADDRESS)
|
|
for {
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
fmt.Println("Error accepting connection: ", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
log.Printf("Connection from %v", conn.RemoteAddr())
|
|
go handler.Handle(conn)
|
|
}
|
|
}
|