mirror of
https://github.com/amigan/aim-oscar-server.git
synced 2024-11-21 12:09:48 -05:00
a little more organization
This commit is contained in:
parent
488ca867c1
commit
298b97f199
11 changed files with 111 additions and 66 deletions
|
@ -8,7 +8,8 @@ Run your own AIM chat server, managing users and groups. Hook up a vintage clien
|
||||||
- [x] Add buddies
|
- [x] Add buddies
|
||||||
- [x] See buddy online/away status
|
- [x] See buddy online/away status
|
||||||
- [x] Chat with buddy
|
- [x] Chat with buddy
|
||||||
- [ ] Set away status
|
- [x] Set away status
|
||||||
|
- [ ] See away status
|
||||||
- [ ] Look up buddy
|
- [ ] Look up buddy
|
||||||
- [ ] Buddy icons
|
- [ ] Buddy icons
|
||||||
- [ ] Rate limiting + warn system
|
- [ ] Rate limiting + warn system
|
||||||
|
|
59
main.go
59
main.go
|
@ -3,6 +3,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"aim-oscar/models"
|
"aim-oscar/models"
|
||||||
"aim-oscar/oscar"
|
"aim-oscar/oscar"
|
||||||
|
"aim-oscar/services"
|
||||||
"aim-oscar/util"
|
"aim-oscar/util"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
@ -12,7 +13,6 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"sync"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -25,37 +25,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SRV_HOST = "10.0.1.2"
|
SRV_HOST = "0.0.0.0"
|
||||||
SRV_PORT = "5190"
|
SRV_PORT = "5190"
|
||||||
SRV_ADDRESS = SRV_HOST + ":" + SRV_PORT
|
SRV_ADDRESS = SRV_HOST + ":" + SRV_PORT
|
||||||
)
|
)
|
||||||
|
|
||||||
var services map[uint16]Service
|
|
||||||
|
|
||||||
// Map username to session
|
|
||||||
var sessions map[string]*oscar.Session
|
|
||||||
var sessionsMutex = &sync.RWMutex{}
|
|
||||||
|
|
||||||
func getSession(username string) *oscar.Session {
|
|
||||||
sessionsMutex.RLock()
|
|
||||||
s, ok := sessions[username]
|
|
||||||
sessionsMutex.RUnlock()
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
services = make(map[uint16]Service)
|
|
||||||
sessions = make(map[string]*oscar.Session)
|
|
||||||
}
|
|
||||||
|
|
||||||
func RegisterService(family uint16, service Service) {
|
|
||||||
services[family] = service
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Set up the DB
|
// Set up the DB
|
||||||
sqldb, err := sql.Open(sqliteshim.ShimName, "file:aim.db")
|
sqldb, err := sql.Open(sqliteshim.ShimName, "file:aim.db")
|
||||||
|
@ -86,14 +60,23 @@ func main() {
|
||||||
}
|
}
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
|
|
||||||
|
sessionManager := NewSessionManager()
|
||||||
|
|
||||||
// Goroutine that listens for messages to deliver and tries to find a user socket to push them to
|
// Goroutine that listens for messages to deliver and tries to find a user socket to push them to
|
||||||
commCh, messageRoutine := MessageDelivery()
|
commCh, messageRoutine := MessageDelivery(sessionManager)
|
||||||
go messageRoutine(db)
|
go messageRoutine(db)
|
||||||
|
|
||||||
// Goroutine that listens for users who change their online status and notifies their buddies
|
// Goroutine that listens for users who change their online status and notifies their buddies
|
||||||
onlineCh, onlineRoutine := OnlineNotification()
|
onlineCh, onlineRoutine := OnlineNotification(sessionManager)
|
||||||
go onlineRoutine(db)
|
go onlineRoutine(db)
|
||||||
|
|
||||||
|
serviceManager := NewServiceManager()
|
||||||
|
serviceManager.RegisterService(0x01, &services.GenericServiceControls{OnlineCh: onlineCh})
|
||||||
|
serviceManager.RegisterService(0x02, &services.LocationServices{OnlineCh: onlineCh})
|
||||||
|
serviceManager.RegisterService(0x03, &services.BuddyListManagement{})
|
||||||
|
serviceManager.RegisterService(0x04, &services.ICBM{CommCh: commCh})
|
||||||
|
serviceManager.RegisterService(0x17, &services.AuthorizationRegistrationService{})
|
||||||
|
|
||||||
handleCloseFn := func(ctx context.Context, session *oscar.Session) {
|
handleCloseFn := func(ctx context.Context, session *oscar.Session) {
|
||||||
log.Printf("%v disconnected", session.RemoteAddr())
|
log.Printf("%v disconnected", session.RemoteAddr())
|
||||||
|
|
||||||
|
@ -108,6 +91,8 @@ func main() {
|
||||||
if true {
|
if true {
|
||||||
onlineCh <- user
|
onlineCh <- user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sessionManager.RemoveSession(user.Username)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +106,7 @@ func main() {
|
||||||
fmt.Printf("%s (%v) ->\n%+v\n", user.Username, session.RemoteAddr(), flap)
|
fmt.Printf("%s (%v) ->\n%+v\n", user.Username, session.RemoteAddr(), flap)
|
||||||
user.LastActivityAt = time.Now()
|
user.LastActivityAt = time.Now()
|
||||||
ctx = models.NewContextWithUser(ctx, user)
|
ctx = models.NewContextWithUser(ctx, user)
|
||||||
sessions[user.Username] = session
|
sessionManager.SetSession(user.Username, session)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%v ->\n%+v\n", session.RemoteAddr(), flap)
|
fmt.Printf("%v ->\n%+v\n", session.RemoteAddr(), flap)
|
||||||
}
|
}
|
||||||
|
@ -132,7 +117,7 @@ func main() {
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := AuthenticateFLAPCookie(ctx, db, flap)
|
user, err := services.AuthenticateFLAPCookie(ctx, db, flap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Could not authenticate cookie: %s", err)
|
log.Printf("Could not authenticate cookie: %s", err)
|
||||||
return ctx
|
return ctx
|
||||||
|
@ -141,7 +126,7 @@ func main() {
|
||||||
|
|
||||||
// Send available services
|
// Send available services
|
||||||
servicesSnac := oscar.NewSNAC(1, 3)
|
servicesSnac := oscar.NewSNAC(1, 3)
|
||||||
for family := range versions {
|
for family := range services.ServiceVersions {
|
||||||
servicesSnac.Data.WriteUint16(family)
|
servicesSnac.Data.WriteUint16(family)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,7 +149,7 @@ func main() {
|
||||||
fmt.Printf("%s\n\n", util.PrettyBytes(snac.Data.Bytes()))
|
fmt.Printf("%s\n\n", util.PrettyBytes(snac.Data.Bytes()))
|
||||||
}
|
}
|
||||||
|
|
||||||
if service, ok := services[snac.Header.Family]; ok {
|
if service, ok := serviceManager.GetService(snac.Header.Family); ok {
|
||||||
newCtx, err := service.HandleSNAC(ctx, db, snac)
|
newCtx, err := service.HandleSNAC(ctx, db, snac)
|
||||||
util.PanicIfError(err)
|
util.PanicIfError(err)
|
||||||
return newCtx
|
return newCtx
|
||||||
|
@ -179,12 +164,6 @@ func main() {
|
||||||
|
|
||||||
handler := oscar.NewHandler(handleFn, handleCloseFn)
|
handler := oscar.NewHandler(handleFn, handleCloseFn)
|
||||||
|
|
||||||
RegisterService(0x01, &GenericServiceControls{OnlineCh: onlineCh})
|
|
||||||
RegisterService(0x02, &LocationServices{OnlineCh: onlineCh})
|
|
||||||
RegisterService(0x03, &BuddyListManagement{})
|
|
||||||
RegisterService(0x04, &ICBM{CommCh: commCh})
|
|
||||||
RegisterService(0x17, &AuthorizationRegistrationService{})
|
|
||||||
|
|
||||||
exitChan := make(chan os.Signal, 1)
|
exitChan := make(chan os.Signal, 1)
|
||||||
signal.Notify(exitChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGABRT)
|
signal.Notify(exitChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGABRT)
|
||||||
go func() {
|
go func() {
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
|
|
||||||
type routineFn func(db *bun.DB)
|
type routineFn func(db *bun.DB)
|
||||||
|
|
||||||
func MessageDelivery() (chan *models.Message, routineFn) {
|
func MessageDelivery(sm *SessionManager) (chan *models.Message, routineFn) {
|
||||||
commCh := make(chan *models.Message, 1)
|
commCh := make(chan *models.Message, 1)
|
||||||
|
|
||||||
routine := func(db *bun.DB) {
|
routine := func(db *bun.DB) {
|
||||||
|
@ -26,7 +26,7 @@ func MessageDelivery() (chan *models.Message, routineFn) {
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("got a message: %s", message)
|
log.Printf("got a message: %s", message)
|
||||||
if s := getSession(message.To); s != nil {
|
if s := sm.GetSession(message.To); s != nil {
|
||||||
messageSnac := oscar.NewSNAC(4, 7)
|
messageSnac := oscar.NewSNAC(4, 7)
|
||||||
messageSnac.Data.WriteUint64(message.Cookie)
|
messageSnac.Data.WriteUint64(message.Cookie)
|
||||||
messageSnac.Data.WriteUint16(1)
|
messageSnac.Data.WriteUint16(1)
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
func OnlineNotification() (chan *models.User, routineFn) {
|
func OnlineNotification(sm *SessionManager) (chan *models.User, routineFn) {
|
||||||
commCh := make(chan *models.User, 1)
|
commCh := make(chan *models.User, 1)
|
||||||
|
|
||||||
routine := func(db *bun.DB) {
|
routine := func(db *bun.DB) {
|
||||||
|
@ -47,7 +47,7 @@ func OnlineNotification() (chan *models.User, routineFn) {
|
||||||
}
|
}
|
||||||
log.Printf("telling %s that %s has a new status: %d!", buddy.Source.Username, user.Username, user.Status)
|
log.Printf("telling %s that %s has a new status: %d!", buddy.Source.Username, user.Username, user.Status)
|
||||||
|
|
||||||
if s := getSession(buddy.Source.Username); s != nil {
|
if s := sm.GetSession(buddy.Source.Username); s != nil {
|
||||||
onlineSnac := oscar.NewSNAC(3, 0xb)
|
onlineSnac := oscar.NewSNAC(3, 0xb)
|
||||||
onlineSnac.Data.WriteLPString(user.Username)
|
onlineSnac.Data.WriteLPString(user.Username)
|
||||||
onlineSnac.Data.WriteUint16(0) // TODO: user warning level
|
onlineSnac.Data.WriteUint16(0) // TODO: user warning level
|
||||||
|
|
20
service_manager.go
Normal file
20
service_manager.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
type ServiceManager struct {
|
||||||
|
services map[uint16]Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServiceManager() *ServiceManager {
|
||||||
|
return &ServiceManager{
|
||||||
|
services: make(map[uint16]Service),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServiceManager) RegisterService(family uint16, service Service) {
|
||||||
|
sm.services[family] = service
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServiceManager) GetService(family uint16) (Service, bool) {
|
||||||
|
s, ok := sm.services[family]
|
||||||
|
return s, ok
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"aim-oscar/aimerror"
|
"aim-oscar/aimerror"
|
||||||
|
@ -13,19 +13,20 @@ import (
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
var versions map[uint16]uint16
|
var ServiceVersions map[uint16]uint16
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
versions = make(map[uint16]uint16)
|
ServiceVersions = make(map[uint16]uint16)
|
||||||
versions[1] = 3
|
ServiceVersions[1] = 3
|
||||||
versions[2] = 1
|
ServiceVersions[2] = 1
|
||||||
versions[3] = 1
|
ServiceVersions[3] = 1
|
||||||
versions[4] = 1
|
ServiceVersions[4] = 1
|
||||||
versions[17] = 1
|
ServiceVersions[17] = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
type GenericServiceControls struct {
|
type GenericServiceControls struct {
|
||||||
OnlineCh chan *models.User
|
OnlineCh chan *models.User
|
||||||
|
ServerHostname string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, snac *oscar.SNAC) (context.Context, error) {
|
func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, snac *oscar.SNAC) (context.Context, error) {
|
||||||
|
@ -63,7 +64,7 @@ func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, sna
|
||||||
tlvs := []*oscar.TLV{
|
tlvs := []*oscar.TLV{
|
||||||
oscar.NewTLV(1, util.Word(0)), // TODO: user class
|
oscar.NewTLV(1, util.Word(0)), // TODO: user class
|
||||||
oscar.NewTLV(0x06, util.Dword(uint32(user.Status))), // TODO: User Status
|
oscar.NewTLV(0x06, util.Dword(uint32(user.Status))), // TODO: User Status
|
||||||
oscar.NewTLV(0x0a, util.Dword(binary.BigEndian.Uint32([]byte(SRV_HOST)))), // External IP
|
oscar.NewTLV(0x0a, util.Dword(binary.BigEndian.Uint32([]byte(g.ServerHostname)))), // External IP
|
||||||
oscar.NewTLV(0x0f, util.Dword(uint32(time.Since(user.LastActivityAt).Seconds()))), // Idle Time
|
oscar.NewTLV(0x0f, util.Dword(uint32(time.Since(user.LastActivityAt).Seconds()))), // Idle Time
|
||||||
oscar.NewTLV(0x03, util.Dword(uint32(time.Now().Unix()))), // Client Signon Time
|
oscar.NewTLV(0x03, util.Dword(uint32(time.Now().Unix()))), // Client Signon Time
|
||||||
oscar.NewTLV(0x05, util.Dword(uint32(user.CreatedAt.Unix()))), // Member since
|
oscar.NewTLV(0x05, util.Dword(uint32(user.CreatedAt.Unix()))), // Member since
|
||||||
|
@ -111,8 +112,8 @@ func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, sna
|
||||||
|
|
||||||
// TODO: make actual rate groups instead of this hack. I can't tell which subtypes are supported so
|
// TODO: make actual rate groups instead of this hack. I can't tell which subtypes are supported so
|
||||||
// make it set rate limits for everything family for all subtypes under 0x21.
|
// make it set rate limits for everything family for all subtypes under 0x21.
|
||||||
rg.WriteUint16(uint16(len(versions)) * 0x21) // Number of rate groups
|
rg.WriteUint16(uint16(len(ServiceVersions)) * 0x21) // Number of rate groups
|
||||||
for family := range versions {
|
for family := range ServiceVersions {
|
||||||
for subtype := 0; subtype < 0x21; subtype++ {
|
for subtype := 0; subtype < 0x21; subtype++ {
|
||||||
rg.WriteUint16(family)
|
rg.WriteUint16(family)
|
||||||
rg.WriteUint16(uint16(subtype))
|
rg.WriteUint16(uint16(subtype))
|
||||||
|
@ -144,7 +145,7 @@ func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, sna
|
||||||
tlvs := []*oscar.TLV{
|
tlvs := []*oscar.TLV{
|
||||||
oscar.NewTLV(0x01, util.Dword(0)), // User Class
|
oscar.NewTLV(0x01, util.Dword(0)), // User Class
|
||||||
oscar.NewTLV(0x06, util.Dword(uint32(user.Status))), // TODO: User Status
|
oscar.NewTLV(0x06, util.Dword(uint32(user.Status))), // TODO: User Status
|
||||||
oscar.NewTLV(0x0a, util.Dword(binary.BigEndian.Uint32([]byte(SRV_HOST)))), // External IP
|
oscar.NewTLV(0x0a, util.Dword(binary.BigEndian.Uint32([]byte(g.ServerHostname)))), // External IP
|
||||||
oscar.NewTLV(0x0f, util.Dword(uint32(time.Since(user.LastActivityAt).Seconds()))), // Idle Time
|
oscar.NewTLV(0x0f, util.Dword(uint32(time.Since(user.LastActivityAt).Seconds()))), // Idle Time
|
||||||
oscar.NewTLV(0x03, util.Dword(uint32(time.Now().Unix()))), // Client Signon Time
|
oscar.NewTLV(0x03, util.Dword(uint32(time.Now().Unix()))), // Client Signon Time
|
||||||
oscar.NewTLV(0x01e, util.Dword(0x0)), // Unknown value
|
oscar.NewTLV(0x01e, util.Dword(0x0)), // Unknown value
|
||||||
|
@ -160,10 +161,10 @@ func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, sna
|
||||||
onlineFlap.Data.WriteBinary(onlineSnac)
|
onlineFlap.Data.WriteBinary(onlineSnac)
|
||||||
return models.NewContextWithUser(ctx, user), session.Send(onlineFlap)
|
return models.NewContextWithUser(ctx, user), session.Send(onlineFlap)
|
||||||
|
|
||||||
// Client wants to know the versions of all of the services offered
|
// Client wants to know the ServiceVersions of all of the services offered
|
||||||
case 0x17:
|
case 0x17:
|
||||||
versionsSnac := oscar.NewSNAC(1, 0x18)
|
versionsSnac := oscar.NewSNAC(1, 0x18)
|
||||||
for family, version := range versions {
|
for family, version := range ServiceVersions {
|
||||||
versionsSnac.Data.WriteUint16(family)
|
versionsSnac.Data.WriteUint16(family)
|
||||||
versionsSnac.Data.WriteUint16(version)
|
versionsSnac.Data.WriteUint16(version)
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"aim-oscar/aimerror"
|
"aim-oscar/aimerror"
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"aim-oscar/aimerror"
|
"aim-oscar/aimerror"
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"aim-oscar/aimerror"
|
"aim-oscar/aimerror"
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -26,7 +26,9 @@ type AuthorizationCookie struct {
|
||||||
X string
|
X string
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthorizationRegistrationService struct{}
|
type AuthorizationRegistrationService struct {
|
||||||
|
ServerHostname string
|
||||||
|
}
|
||||||
|
|
||||||
func AuthenticateFLAPCookie(ctx context.Context, db *bun.DB, flap *oscar.FLAP) (*models.User, error) {
|
func AuthenticateFLAPCookie(ctx context.Context, db *bun.DB, flap *oscar.FLAP) (*models.User, error) {
|
||||||
// Otherwise this is a protocol negotiation from the client. They're likely trying to connect
|
// Otherwise this is a protocol negotiation from the client. They're likely trying to connect
|
||||||
|
@ -174,7 +176,7 @@ func (a *AuthorizationRegistrationService) HandleSNAC(ctx context.Context, db *b
|
||||||
// Send BOS response + cookie
|
// Send BOS response + cookie
|
||||||
authSnac := oscar.NewSNAC(0x17, 0x3)
|
authSnac := oscar.NewSNAC(0x17, 0x3)
|
||||||
authSnac.Data.WriteBinary(usernameTLV)
|
authSnac.Data.WriteBinary(usernameTLV)
|
||||||
authSnac.Data.WriteBinary(oscar.NewTLV(0x5, []byte(SRV_ADDRESS)))
|
authSnac.Data.WriteBinary(oscar.NewTLV(0x5, []byte(a.ServerHostname)))
|
||||||
|
|
||||||
cookie, err := json.Marshal(AuthorizationCookie{
|
cookie, err := json.Marshal(AuthorizationCookie{
|
||||||
UIN: user.UIN,
|
UIN: user.UIN,
|
42
session_manager.go
Normal file
42
session_manager.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"aim-oscar/oscar"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionManager struct {
|
||||||
|
sessions map[string]*oscar.Session
|
||||||
|
mutex *sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSessionManager() *SessionManager {
|
||||||
|
sm := &SessionManager{
|
||||||
|
sessions: make(map[string]*oscar.Session),
|
||||||
|
mutex: &sync.RWMutex{},
|
||||||
|
}
|
||||||
|
return sm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *SessionManager) SetSession(username string, session *oscar.Session) {
|
||||||
|
sm.mutex.Lock()
|
||||||
|
sm.sessions[username] = session
|
||||||
|
sm.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *SessionManager) GetSession(username string) *oscar.Session {
|
||||||
|
sm.mutex.RLock()
|
||||||
|
s, ok := sm.sessions[username]
|
||||||
|
sm.mutex.RUnlock()
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *SessionManager) RemoveSession(username string) {
|
||||||
|
sm.mutex.Lock()
|
||||||
|
sm.sessions[username] = nil
|
||||||
|
sm.mutex.Unlock()
|
||||||
|
}
|
Loading…
Reference in a new issue