Skip to content

Commit

Permalink
Merge pull request #45 from jgknight/admin-service-formatsn
Browse files Browse the repository at this point in the history
Add admin service, support screen name formatting
  • Loading branch information
mk6i authored Jun 29, 2024
2 parents b4e8f88 + b7280a4 commit 840ca35
Show file tree
Hide file tree
Showing 26 changed files with 1,681 additions and 4 deletions.
8 changes: 7 additions & 1 deletion .mockery.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ packages:
PermitDenyService:
config:
filename: "mock_permit_deny_test.go"
AdminService:
config:
filename: "mock_admin_test.go"
github.com/mk6i/retro-aim-server/foodgroup:
interfaces:
FeedbagManager:
Expand Down Expand Up @@ -106,4 +109,7 @@ packages:
filename: "mock_cookie_baker_test.go"
buddyBroadcaster:
config:
filename: "mock_buddy_broadcaster_test.go"
filename: "mock_buddy_broadcaster_test.go"
AccountManager:
config:
filename: "mock_account_manager_test.go"
20 changes: 20 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,26 @@ func main() {
}.Start()
wg.Done()
}(logger)
go func(logger *slog.Logger) {
logger = logger.With("svc", "ADMIN")
buddyService := foodgroup.NewBuddyService(sessionManager, feedbagStore, adjListBuddyListStore)
adminService := foodgroup.NewAdminService(sessionManager, feedbagStore, buddyService)
authService := foodgroup.NewAuthService(cfg, sessionManager, chatSessionManager, feedbagStore, adjListBuddyListStore, cookieBaker, sessionManager, feedbagStore, chatSessionManager)
oServiceService := foodgroup.NewOServiceServiceForAdmin(cfg, logger, buddyService)

oscar.AdminServer{
AuthService: authService,
Config: cfg,
Handler: handler.NewAdminRouter(handler.Handlers{
AdminHandler: handler.NewAdminHandler(logger, adminService),
OServiceHandler: handler.NewOServiceHandler(logger, oServiceService),
}),
Logger: logger,
OnlineNotifier: oServiceService,
ListenAddr: net.JoinHostPort("", cfg.AdminPort),
}.Start()
wg.Done()
}(logger)
go func(logger *slog.Logger) {
logger = logger.With("svc", "BART")
sessionManager := state.NewInMemorySessionManager(logger)
Expand Down
3 changes: 2 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ type Config struct {
BOSPort string `envconfig:"BOS_PORT" required:"true" val:"5191" description:"The port that the BOS service binds to."`
ChatNavPort string `envconfig:"CHAT_NAV_PORT" required:"true" val:"5193" description:"The port that the chat nav service binds to."`
ChatPort string `envconfig:"CHAT_PORT" required:"true" val:"5192" description:"The port that the chat service binds to."`
AdminPort string `envconfig:"ADMIN_PORT" required:"true" val:"5196" description:"The port that the admin service binds to."`
DBPath string `envconfig:"DB_PATH" required:"true" val:"oscar.sqlite" description:"The path to the SQLite database file. The file and DB schema are auto-created if they doesn't exist."`
DisableAuth bool `envconfig:"DISABLE_AUTH" required:"true" val:"true" description:"Disable password check and auto-create new users at login time. Useful for quickly creating new accounts during development without having to register new users via the management API."`
FailFast bool `envconfig:"FAIL_FAST" required:"true" val:"false" description:"Crash the server in case it encounters a client message type it doesn't recognize. This makes failures obvious for debugging purposes."`
LogLevel string `envconfig:"LOG_LEVEL" required:"true" val:"info" description:"Set logging granularity. Possible values: 'trace', 'debug', 'info', 'warn', 'error'."`
OSCARHost string `envconfig:"OSCAR_HOST" required:"true" val:"127.0.0.1" description:"The hostname that AIM clients connect to in order to reach OSCAR services (auth, BOS, BUCP, etc). Make sure the hostname is reachable by all clients. For local development, the default loopback address should work provided the server and AIM client(s) are running on the same machine. For LAN-only clients, a private IP address (e.g. 192.168..) or hostname should suffice. For clients connecting over the Internet, specify your public IP address and ensure that TCP ports 5190-5194 are open on your firewall."`
OSCARHost string `envconfig:"OSCAR_HOST" required:"true" val:"127.0.0.1" description:"The hostname that AIM clients connect to in order to reach OSCAR services (auth, BOS, BUCP, etc). Make sure the hostname is reachable by all clients. For local development, the default loopback address should work provided the server and AIM client(s) are running on the same machine. For LAN-only clients, a private IP address (e.g. 192.168..) or hostname should suffice. For clients connecting over the Internet, specify your public IP address and ensure that TCP ports 5190-5196 are open on your firewall."`
}
2 changes: 2 additions & 0 deletions config/ras.service
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ Description=Retro AIM Server
Type=simple
User=ras
Group=ras
Environment="ADMIN_PORT=5196"
Environment="ALERT_PORT=5194"
Environment="AUTH_PORT=5190"
Environment="BART_PORT=5195"
Environment="BOS_PORT=5191"
Environment="CHAT_NAV_PORT=5193"
Environment="CHAT_PORT=5192"
Expand Down
5 changes: 4 additions & 1 deletion config/settings.bat
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ set CHAT_NAV_PORT=5193
rem The port that the chat service binds to.
set CHAT_PORT=5192

rem The port that the admin service binds to.
set ADMIN_PORT=5196

rem The path to the SQLite database file. The file and DB schema are
rem auto-created if they doesn't exist.
set DB_PATH=oscar.sqlite
Expand All @@ -45,6 +48,6 @@ rem For local development, the default loopback address should work provided the
rem server and AIM client(s) are running on the same machine. For LAN-only
rem clients, a private IP address (e.g. 192.168..) or hostname should suffice.
rem For clients connecting over the Internet, specify your public IP address and
rem ensure that TCP ports 5190-5194 are open on your firewall.
rem ensure that TCP ports 5190-5196 are open on your firewall.
set OSCAR_HOST=127.0.0.1

5 changes: 4 additions & 1 deletion config/settings.env
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export CHAT_NAV_PORT=5193
# The port that the chat service binds to.
export CHAT_PORT=5192

# The port that the admin service binds to.
export ADMIN_PORT=5196

# The path to the SQLite database file. The file and DB schema are auto-created
# if they doesn't exist.
export DB_PATH=oscar.sqlite
Expand All @@ -45,6 +48,6 @@ export LOG_LEVEL=info
# server and AIM client(s) are running on the same machine. For LAN-only
# clients, a private IP address (e.g. 192.168..) or hostname should suffice. For
# clients connecting over the Internet, specify your public IP address and
# ensure that TCP ports 5190-5194 are open on your firewall.
# ensure that TCP ports 5190-5196 are open on your firewall.
export OSCAR_HOST=127.0.0.1

160 changes: 160 additions & 0 deletions foodgroup/admin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package foodgroup

import (
"context"

"github.com/mk6i/retro-aim-server/state"
"github.com/mk6i/retro-aim-server/wire"
)

// NewAdminService creates an instance of AdminService.
func NewAdminService(
sessionManager SessionManager,
accountManager AccountManager,
buddyUpdateBroadcaster buddyBroadcaster,
) *AdminService {
return &AdminService{
sessionManager: sessionManager,
accountManager: accountManager,
buddyUpdateBroadcaster: buddyUpdateBroadcaster,
}
}

// AdminService provides functionality for the Admin food group.
// The Admin food group is used for client control of passwords, screen name formatting,
// email address, and account confirmation.
type AdminService struct {
sessionManager SessionManager
accountManager AccountManager
buddyUpdateBroadcaster buddyBroadcaster
}

// ConfirmRequest returns the ScreenName account status. It returns SNAC
// wire.AdminConfirmReply. The values in the return SNAC are
// flag, URL, length
func (s AdminService) ConfirmRequest(_ context.Context, frame wire.SNACFrame) (wire.SNACMessage, error) {
return wire.SNACMessage{
Frame: wire.SNACFrame{
FoodGroup: wire.Admin,
SubGroup: wire.AdminAcctConfirmReply,
RequestID: frame.RequestID,
},
Body: wire.SNAC_0x07_0x07_AdminConfirmReply{
Status: wire.AdminAcctConfirmStatusEmailSent, // todo: get from session/db
},
}, nil
}

// InfoQuery returns the requested information about the account
func (s AdminService) InfoQuery(_ context.Context, sess *state.Session, frame wire.SNACFrame, body wire.SNAC_0x07_0x02_AdminInfoQuery) (wire.SNACMessage, error) {
var getAdminInfoReply = func(tag uint16, val any) wire.SNACMessage {
return wire.SNACMessage{
Frame: wire.SNACFrame{
FoodGroup: wire.Admin,
SubGroup: wire.AdminInfoReply,
RequestID: frame.RequestID,
},
Body: wire.SNAC_0x07_0x03_AdminInfoReply{
Permissions: wire.AdminInfoPermissionsReadWrite, // todo: what does this actually control?
TLVBlock: wire.TLVBlock{
TLVList: wire.TLVList{
wire.NewTLV(tag, val),
},
},
},
}
}

// wire.AdminTLVRegistrationStatus is used in the AIM Preferences > Privacy panel to control
// Allow users who know my e-mail address to find...
// o Nothing about me - wire.AdminInfoRegStatusNoDisclosure
// o Only that I have an account - wire.AdminInfoRegStatusLimitDisclosure
// o My screen name - wire.AdminInfoRegStatusFullDisclosure
if _, hasRegStatus := body.TLVRestBlock.Slice(wire.AdminTLVRegistrationStatus); hasRegStatus {
return getAdminInfoReply(wire.AdminTLVRegistrationStatus, wire.AdminInfoRegStatusFullDisclosure), nil // todo: get from session/db

} else if _, hasEmail := body.TLVRestBlock.Slice(wire.AdminTLVEmailAddress); hasEmail {
return getAdminInfoReply(wire.AdminTLVEmailAddress, sess.IdentScreenName().String()+"@aol.com"), nil // todo: get from session/db

} else if _, hasNickName := body.TLVRestBlock.Slice(wire.AdminTLVScreenNameFormatted); hasNickName {
return getAdminInfoReply(wire.AdminTLVScreenNameFormatted, sess.DisplayScreenName().String()), nil

}

return wire.SNACMessage{
Frame: wire.SNACFrame{
FoodGroup: wire.Admin,
SubGroup: wire.AdminErr,
RequestID: frame.RequestID,
},
Body: wire.SNACError{
Code: wire.ErrorCodeNotSupportedByHost,
},
}, nil
}

func (s AdminService) InfoChangeRequest(ctx context.Context, sess *state.Session, frame wire.SNACFrame, body wire.SNAC_0x07_0x04_AdminInfoChangeRequest) (wire.SNACMessage, error) {
var replyMessage = func(tag uint16, val any) wire.SNACMessage {
return wire.SNACMessage{
Frame: wire.SNACFrame{
FoodGroup: wire.Admin,
SubGroup: wire.AdminInfoChangeReply,
RequestID: frame.RequestID,
},
Body: wire.SNAC_0x07_0x05_AdminChangeReply{
Permissions: wire.AdminInfoPermissionsReadWrite,
TLVBlock: wire.TLVBlock{
TLVList: wire.TLVList{
wire.NewTLV(tag, val),
},
},
},
}
}

var validateProposedName = func(name state.DisplayScreenName) (ok bool, errorCode uint16) {
if len(name) > 16 {
// proposed name is too long
// todo: 16 should be defined elsewhere
return false, wire.AdminInfoErrorInvalidNickNameLength
} else if name.IdentScreenName() != sess.IdentScreenName() {
// proposed name does not match session name (e.g. malicious client)
return false, wire.AdminInfoErrorInvalidNickName
}
return true, 0
}

if sn, hasScreenNameFormatted := body.TLVRestBlock.Slice(wire.AdminTLVScreenNameFormatted); hasScreenNameFormatted {
proposedName := state.DisplayScreenName(sn)
if ok, errorCode := validateProposedName(proposedName); !ok {
return wire.SNACMessage{
Frame: wire.SNACFrame{
FoodGroup: wire.Admin,
SubGroup: wire.AdminErr,
RequestID: frame.RequestID,
},
Body: wire.SNACError{
Code: errorCode,
},
}, nil
}
if err := s.accountManager.UpdateDisplayScreenName(proposedName); err != nil {
return wire.SNACMessage{}, err
}
sess.SetDisplayScreenName(proposedName)
if err := s.buddyUpdateBroadcaster.BroadcastBuddyArrived(ctx, sess); err != nil {
return wire.SNACMessage{}, err
}
return replyMessage(wire.AdminTLVScreenNameFormatted, proposedName.String()), nil
}
return wire.SNACMessage{
Frame: wire.SNACFrame{
FoodGroup: wire.Admin,
SubGroup: wire.AdminErr,
RequestID: frame.RequestID,
},
Body: wire.SNACError{
Code: wire.ErrorCodeNotSupportedByHost,
},
}, nil
}
Loading

0 comments on commit 840ca35

Please sign in to comment.