Mike 11 месяцев назад
Родитель
Сommit
760fa3a442
6 измененных файлов с 298 добавлено и 0 удалено
  1. 78 0
      cmd/server/factory.go
  2. 11 0
      cmd/server/main.go
  3. 0 0
      docs/open_api/webapi.yml
  4. 32 0
      server/webapi/handler.go
  5. 71 0
      server/webapi/server.go
  6. 106 0
      server/webapi/types.go

+ 78 - 0
cmd/server/factory.go

@@ -19,6 +19,7 @@ import (
 	"github.com/mk6i/retro-aim-server/server/oscar"
 	"github.com/mk6i/retro-aim-server/server/oscar/middleware"
 	"github.com/mk6i/retro-aim-server/server/toc"
+	"github.com/mk6i/retro-aim-server/server/webapi"
 	"github.com/mk6i/retro-aim-server/state"
 	"github.com/mk6i/retro-aim-server/wire"
 )
@@ -420,3 +421,80 @@ func TOC(deps Container) *toc.Server {
 		toc.NewIPRateLimiter(rate.Every(1*time.Minute), 10, 1*time.Minute),
 	)
 }
+
+// WebAPI creates an HTTP server for the webapi protocol.
+func WebAPI(deps Container) *webapi.Server {
+	logger := deps.logger.With("svc", "webapi")
+	handler := webapi.Handler{
+		AdminService: foodgroup.NewAdminService(
+			deps.sqLiteUserStore,
+			deps.sqLiteUserStore,
+			deps.sqLiteUserStore,
+			deps.inMemorySessionManager,
+			deps.inMemorySessionManager,
+			deps.logger,
+		),
+		AuthService: foodgroup.NewAuthService(
+			deps.cfg,
+			deps.inMemorySessionManager,
+			deps.inMemorySessionManager,
+			deps.chatSessionManager,
+			deps.sqLiteUserStore,
+			deps.hmacCookieBaker,
+			deps.chatSessionManager,
+			deps.sqLiteUserStore,
+			deps.rateLimitClasses,
+		),
+		BuddyListRegistry: deps.sqLiteUserStore,
+		BuddyService: foodgroup.NewBuddyService(
+			deps.inMemorySessionManager,
+			deps.sqLiteUserStore,
+			deps.sqLiteUserStore,
+			deps.inMemorySessionManager,
+			deps.sqLiteUserStore,
+		),
+		CookieBaker:      deps.hmacCookieBaker,
+		DirSearchService: foodgroup.NewODirService(logger, deps.sqLiteUserStore),
+		ICBMService: foodgroup.NewICBMService(
+			deps.sqLiteUserStore,
+			deps.inMemorySessionManager,
+			deps.sqLiteUserStore,
+			deps.sqLiteUserStore,
+			deps.inMemorySessionManager,
+			deps.snacRateLimits,
+		),
+		LocateService: foodgroup.NewLocateService(
+			deps.sqLiteUserStore,
+			deps.inMemorySessionManager,
+			deps.sqLiteUserStore,
+			deps.sqLiteUserStore,
+			deps.inMemorySessionManager,
+		),
+		Logger: logger,
+		OServiceService: foodgroup.NewOServiceService(
+			deps.cfg,
+			deps.inMemorySessionManager,
+			logger,
+			deps.hmacCookieBaker,
+			deps.sqLiteUserStore,
+			deps.sqLiteUserStore,
+			deps.inMemorySessionManager,
+			deps.sqLiteUserStore,
+			deps.rateLimitClasses,
+			deps.snacRateLimits,
+			deps.chatSessionManager,
+		),
+		PermitDenyService: foodgroup.NewPermitDenyService(
+			deps.sqLiteUserStore,
+			deps.sqLiteUserStore,
+			deps.sqLiteUserStore,
+			deps.inMemorySessionManager,
+			deps.inMemorySessionManager,
+		),
+		TOCConfigStore: deps.sqLiteUserStore,
+		ChatService:    foodgroup.NewChatService(deps.chatSessionManager),
+		ChatNavService: foodgroup.NewChatNavService(logger, deps.sqLiteUserStore),
+		SNACRateLimits: deps.snacRateLimits,
+	}
+	return webapi.NewServer([]string{"0.0.0.0:8081"}, logger, handler)
+}

+ 11 - 0
cmd/server/main.go

@@ -11,6 +11,8 @@ import (
 
 	"github.com/joho/godotenv"
 	"golang.org/x/sync/errgroup"
+
+	"github.com/mk6i/retro-aim-server/server/webapi"
 )
 
 var (
@@ -70,6 +72,12 @@ func main() {
 	toc := TOC(deps)
 	g.Go(toc.ListenAndServe)
 
+	var webAPI *webapi.Server
+	if os.Getenv("ENABLE_WEBAPI") == "1" {
+		webAPI = WebAPI(deps)
+		g.Go(webAPI.ListenAndServe)
+	}
+
 	select {
 	case <-ctx.Done():
 		shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@@ -78,6 +86,9 @@ func main() {
 		_ = kerb.Shutdown(shutdownCtx)
 		_ = api.Shutdown(shutdownCtx)
 		_ = toc.Shutdown(shutdownCtx)
+		if os.Getenv("ENABLE_WEBAPI") == "1" {
+			_ = webAPI.Shutdown(shutdownCtx)
+		}
 	}
 
 	if err = g.Wait(); err != nil {

+ 0 - 0
webaim/web-aim-server.yaml → docs/open_api/webapi.yml


+ 32 - 0
server/webapi/handler.go

@@ -0,0 +1,32 @@
+package webapi
+
+import (
+	"fmt"
+	"log/slog"
+	"net/http"
+
+	"github.com/mk6i/retro-aim-server/wire"
+)
+
+type Handler struct {
+	AdminService      AdminService
+	AuthService       AuthService
+	BuddyListRegistry BuddyListRegistry
+	BuddyService      BuddyService
+	ChatNavService    ChatNavService
+	ChatService       ChatService
+	CookieBaker       CookieBaker
+	DirSearchService  DirSearchService
+	ICBMService       ICBMService
+	LocateService     LocateService
+	Logger            *slog.Logger
+	OServiceService   OServiceService
+	PermitDenyService PermitDenyService
+	TOCConfigStore    TOCConfigStore
+	SNACRateLimits    wire.SNACRateLimits
+}
+
+func (h Handler) GetHelloWorldHandler(w http.ResponseWriter, r *http.Request) {
+	h.Logger.Info("got a Hello World request!")
+	_, _ = fmt.Fprintln(w, "Hello ukozi!")
+}

+ 71 - 0
server/webapi/server.go

@@ -0,0 +1,71 @@
+package webapi
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"log/slog"
+	"net/http"
+
+	"golang.org/x/sync/errgroup"
+)
+
+func NewServer(listeners []string, logger *slog.Logger, handler Handler) *Server {
+	servers := make([]*http.Server, 0, len(listeners))
+
+	for _, l := range listeners {
+		mux := http.NewServeMux()
+		mux.HandleFunc("GET /", handler.GetHelloWorldHandler)
+
+		servers = append(servers, &http.Server{
+			Addr:    l,
+			Handler: mux,
+		})
+	}
+
+	return &Server{
+		servers: servers,
+		logger:  logger,
+	}
+}
+
+// Server hosts an HTTP endpoint capable of handling AIM-style Kerberos
+// authentication. The messages are structured as SNACs transmitted over HTTP.
+type Server struct {
+	servers []*http.Server
+	logger  *slog.Logger
+}
+
+func (s *Server) ListenAndServe() error {
+	if len(s.servers) == 0 {
+		s.logger.Debug("no webapi listeners defined")
+		return nil
+	}
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	g, ctx := errgroup.WithContext(ctx)
+	for _, server := range s.servers {
+		g.Go(func() error {
+			s.logger.Info("starting server", "addr", server.Addr)
+			if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
+				cancel()
+				return fmt.Errorf("unable to start webapi server: %w", err)
+			}
+			return nil
+		})
+	}
+
+	return g.Wait()
+}
+
+func (s *Server) Shutdown(ctx context.Context) error {
+	if len(s.servers) > 0 {
+		for _, srv := range s.servers {
+			_ = srv.Shutdown(ctx)
+		}
+		s.logger.Info("shutdown complete")
+	}
+	return nil
+}

+ 106 - 0
server/webapi/types.go

@@ -0,0 +1,106 @@
+package webapi
+
+import (
+	"context"
+
+	"github.com/google/uuid"
+	"github.com/mk6i/retro-aim-server/config"
+
+	"github.com/mk6i/retro-aim-server/state"
+	"github.com/mk6i/retro-aim-server/wire"
+)
+
+type BuddyService interface {
+	AddBuddies(ctx context.Context, sess *state.Session, inBody wire.SNAC_0x03_0x04_BuddyAddBuddies) error
+	BroadcastBuddyDeparted(ctx context.Context, sess *state.Session) error
+	DelBuddies(ctx context.Context, sess *state.Session, inBody wire.SNAC_0x03_0x05_BuddyDelBuddies) error
+	RightsQuery(ctx context.Context, inFrame wire.SNACFrame) wire.SNACMessage
+}
+
+type ChatService interface {
+	ChannelMsgToHost(ctx context.Context, sess *state.Session, inFrame wire.SNACFrame, inBody wire.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*wire.SNACMessage, error)
+}
+
+type ChatNavService interface {
+	CreateRoom(ctx context.Context, sess *state.Session, inFrame wire.SNACFrame, inBody wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (wire.SNACMessage, error)
+	ExchangeInfo(ctx context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x0D_0x03_ChatNavRequestExchangeInfo) (wire.SNACMessage, error)
+	RequestChatRights(ctx context.Context, inFrame wire.SNACFrame) wire.SNACMessage
+	RequestRoomInfo(ctx context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (wire.SNACMessage, error)
+}
+
+type ICBMService interface {
+	ChannelMsgToHost(ctx context.Context, sess *state.Session, inFrame wire.SNACFrame, inBody wire.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*wire.SNACMessage, error)
+	ClientEvent(ctx context.Context, sess *state.Session, inFrame wire.SNACFrame, inBody wire.SNAC_0x04_0x14_ICBMClientEvent) error
+	EvilRequest(ctx context.Context, sess *state.Session, inFrame wire.SNACFrame, inBody wire.SNAC_0x04_0x08_ICBMEvilRequest) (wire.SNACMessage, error)
+	ParameterQuery(ctx context.Context, inFrame wire.SNACFrame) wire.SNACMessage
+	ClientErr(ctx context.Context, sess *state.Session, frame wire.SNACFrame, body wire.SNAC_0x04_0x0B_ICBMClientErr) error
+}
+
+type OServiceService interface {
+	ClientOnline(ctx context.Context, service uint16, bodyIn wire.SNAC_0x01_0x02_OServiceClientOnline, sess *state.Session) error
+	IdleNotification(ctx context.Context, sess *state.Session, bodyIn wire.SNAC_0x01_0x11_OServiceIdleNotification) error
+	ServiceRequest(ctx context.Context, service uint16, sess *state.Session, frame wire.SNACFrame, bodyIn wire.SNAC_0x01_0x04_OServiceServiceRequest, listener config.Listener) (wire.SNACMessage, error)
+}
+
+type AuthService interface {
+	BUCPChallenge(ctx context.Context, bodyIn wire.SNAC_0x17_0x06_BUCPChallengeRequest, newUUID func() uuid.UUID) (wire.SNACMessage, error)
+	BUCPLogin(ctx context.Context, bodyIn wire.SNAC_0x17_0x02_BUCPLoginRequest, newUserFn func(screenName state.DisplayScreenName) (state.User, error), advertisedHost string) (wire.SNACMessage, error)
+	CrackCookie(authCookie []byte) (state.ServerCookie, error)
+	FLAPLogin(ctx context.Context, frame wire.FLAPSignonFrame, newUserFn func(screenName state.DisplayScreenName) (state.User, error), here string) (wire.TLVRestBlock, error)
+	RegisterBOSSession(ctx context.Context, authCookie state.ServerCookie) (*state.Session, error)
+	RegisterChatSession(ctx context.Context, authCookie state.ServerCookie) (*state.Session, error)
+	RetrieveBOSSession(ctx context.Context, authCookie state.ServerCookie) (*state.Session, error)
+	Signout(ctx context.Context, sess *state.Session)
+	SignoutChat(ctx context.Context, sess *state.Session)
+}
+
+type LocateService interface {
+	SetDirInfo(ctx context.Context, sess *state.Session, inFrame wire.SNACFrame, inBody wire.SNAC_0x02_0x09_LocateSetDirInfo) (wire.SNACMessage, error)
+	SetInfo(ctx context.Context, sess *state.Session, inBody wire.SNAC_0x02_0x04_LocateSetInfo) error
+	UserInfoQuery(ctx context.Context, sess *state.Session, inFrame wire.SNACFrame, inBody wire.SNAC_0x02_0x05_LocateUserInfoQuery) (wire.SNACMessage, error)
+	DirInfo(ctx context.Context, inFrame wire.SNACFrame, body wire.SNAC_0x02_0x0B_LocateGetDirInfo) (wire.SNACMessage, error)
+}
+
+type DirSearchService interface {
+	InfoQuery(_ context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x0F_0x02_InfoQuery) (wire.SNACMessage, error)
+}
+
+type PermitDenyService interface {
+	AddDenyListEntries(ctx context.Context, sess *state.Session, body wire.SNAC_0x09_0x07_PermitDenyAddDenyListEntries) error
+	AddPermListEntries(ctx context.Context, sess *state.Session, body wire.SNAC_0x09_0x05_PermitDenyAddPermListEntries) error
+	DelDenyListEntries(ctx context.Context, sess *state.Session, body wire.SNAC_0x09_0x08_PermitDenyDelDenyListEntries) error
+	DelPermListEntries(ctx context.Context, sess *state.Session, body wire.SNAC_0x09_0x06_PermitDenyDelPermListEntries) error
+	RightsQuery(_ context.Context, frame wire.SNACFrame) wire.SNACMessage
+}
+
+// BuddyListRegistry is the interface for keeping track of users with active
+// buddy lists. Once registered, a user becomes visible to other users' buddy
+// lists and vice versa.
+type BuddyListRegistry interface {
+	RegisterBuddyList(ctx context.Context, user state.IdentScreenName) error
+	UnregisterBuddyList(ctx context.Context, user state.IdentScreenName) error
+}
+
+type TOCConfigStore interface {
+	// SetTOCConfig sets the user's TOC config. The TOC config is the server-side
+	// buddy list functionality for TOC. This configuration is not available to
+	// OSCAR clients.
+	SetTOCConfig(ctx context.Context, user state.IdentScreenName, config string) error
+	User(ctx context.Context, screenName state.IdentScreenName) (*state.User, error)
+}
+
+// CookieBaker defines methods for issuing and verifying AIM authentication tokens ("cookies").
+// These tokens are used for authenticating client sessions with AIM services.
+type CookieBaker interface {
+	// Crack verifies and decodes a previously issued authentication token.
+	// Returns the original payload if the token is valid.
+	Crack(data []byte) ([]byte, error)
+
+	// Issue creates a new authentication token from the given payload.
+	// The resulting token can later be verified using Crack.
+	Issue(data []byte) ([]byte, error)
+}
+
+type AdminService interface {
+	InfoChangeRequest(ctx context.Context, sess *state.Session, frame wire.SNACFrame, body wire.SNAC_0x07_0x04_AdminInfoChangeRequest) (wire.SNACMessage, error)
+}