diff --git a/server/internal/http/legacy/handler.go b/server/internal/http/legacy/handler.go index 7b2006ae..c450d2e2 100644 --- a/server/internal/http/legacy/handler.go +++ b/server/internal/http/legacy/handler.go @@ -1,12 +1,17 @@ package legacy import ( + "encoding/json" "errors" "fmt" "net/http" + "time" + "m1k1o/neko/internal/api" + "m1k1o/neko/internal/api/room" oldEvent "m1k1o/neko/internal/http/legacy/event" oldMessage "m1k1o/neko/internal/http/legacy/message" + oldTypes "m1k1o/neko/internal/http/legacy/types" "m1k1o/neko/pkg/types" "m1k1o/neko/pkg/types/event" @@ -36,6 +41,7 @@ var ( type LegacyHandler struct { logger zerolog.Logger serverAddr string + startedAt time.Time } func New() *LegacyHandler { @@ -44,6 +50,7 @@ func New() *LegacyHandler { return &LegacyHandler{ logger: log.With().Str("module", "legacy").Logger(), serverAddr: "127.0.0.1:8080", + startedAt: time.Now(), } } @@ -64,7 +71,7 @@ func (h *LegacyHandler) Route(r types.Router) { // create a new session username := r.URL.Query().Get("username") password := r.URL.Query().Get("password") - token, err := s.create(username, password) + err = s.create(username, password) if err != nil { h.logger.Error().Err(err).Msg("couldn't create a new session") @@ -80,7 +87,7 @@ func (h *LegacyHandler) Route(r types.Router) { defer s.destroy() // dial to the remote backend - connBackend, _, err := DefaultDialer.Dial("ws://"+h.serverAddr+"/api/ws?token="+token, nil) + connBackend, _, err := DefaultDialer.Dial("ws://"+h.serverAddr+"/api/ws?token="+s.token, nil) if err != nil { h.logger.Error().Err(err).Msg("couldn't dial to the remote backend") @@ -174,24 +181,90 @@ func (h *LegacyHandler) Route(r types.Router) { return nil }) + r.Get("/stats", func(w http.ResponseWriter, r *http.Request) error { + s := newSession(h.logger, h.serverAddr) + + // create a new session + username := r.URL.Query().Get("usr") + password := r.URL.Query().Get("pwd") + err := s.create(username, password) + if err != nil { + return utils.HttpForbidden(err.Error()) + } + defer s.destroy() + + if !s.isAdmin { + return utils.HttpUnauthorized().Msg("bad authorization") + } + + w.Header().Set("Content-Type", "application/json") + + // get all sessions + sessions := []api.SessionDataPayload{} + err = s.apiReq(http.MethodGet, "/api/sessions", nil, &sessions) + if err != nil { + return utils.HttpInternalServerError().WithInternalErr(err) + } + + // get current control status + control := room.ControlStatusPayload{} + err = s.apiReq(http.MethodGet, "/api/room/control", nil, &control) + if err != nil { + return utils.HttpInternalServerError().WithInternalErr(err) + } + + // get settings + settings := types.Settings{} + err = s.apiReq(http.MethodGet, "/api/room/settings", nil, &settings) + if err != nil { + return utils.HttpInternalServerError().WithInternalErr(err) + } + + var stats oldTypes.Stats + + // create empty array so that it's not null in json + stats.Members = []*oldTypes.Member{} + + for _, session := range sessions { + if session.State.IsConnected { + stats.Connections++ + member, err := profileToMember(session.ID, session.Profile) + if err != nil { + return utils.HttpInternalServerError().WithInternalErr(err) + } + // append members + stats.Members = append(stats.Members, member) + } else if session.State.NotConnectedSince != nil { + // + // TODO: This wont work if the user is removed after the session is closed + // + // populate last admin left time + if session.Profile.IsAdmin && (stats.LastAdminLeftAt == nil || (*session.State.NotConnectedSince).After(*stats.LastAdminLeftAt)) { + stats.LastAdminLeftAt = session.State.NotConnectedSince + } + // populate last user left time + if !session.Profile.IsAdmin && (stats.LastUserLeftAt == nil || (*session.State.NotConnectedSince).After(*stats.LastUserLeftAt)) { + stats.LastUserLeftAt = session.State.NotConnectedSince + } + } + } + + locks, err := s.settingsToLocks(settings) + if err != nil { + return err + } + + stats.Host = control.HostId + // TODO: stats.Banned, not implemented yet + stats.Locked = locks + stats.ServerStartedAt = h.startedAt + stats.ControlProtection = settings.ControlProtection + stats.ImplicitControl = settings.ImplicitHosting + + return json.NewEncoder(w).Encode(stats) + }) + /* - r.Get("/stats", func(w http.ResponseWriter, r *http.Request) error { - password := r.URL.Query().Get("pwd") - isAdmin, err := webSocketHandler.IsAdmin(password) - if err != nil { - return utils.HttpForbidden(err) - } - - if !isAdmin { - return utils.HttpUnauthorized().Msg("bad authorization") - } - - w.Header().Set("Content-Type", "application/json") - - stats := webSocketHandler.Stats() - return json.NewEncoder(w).Encode(stats) - }) - r.Get("/screenshot.jpg", func(w http.ResponseWriter, r *http.Request) error { password := r.URL.Query().Get("pwd") isAdmin, err := webSocketHandler.IsAdmin(password) diff --git a/server/internal/http/legacy/session.go b/server/internal/http/legacy/session.go index 0e7d7273..feaa36fc 100644 --- a/server/internal/http/legacy/session.go +++ b/server/internal/http/legacy/session.go @@ -32,10 +32,11 @@ type session struct { logger zerolog.Logger serverAddr string - id string - token string - name string - client *http.Client + id string + token string + name string + isAdmin bool + client *http.Client lastHostID string lockedControls bool @@ -142,7 +143,7 @@ func (s *session) toBackend(event string, payload any) error { return nil } -func (s *session) create(username, password string) (string, error) { +func (s *session) create(username, password string) error { data := api.SessionDataPayload{} err := s.apiReq(http.MethodPost, "/api/login", api.SessionLoginPayload{ @@ -150,19 +151,20 @@ func (s *session) create(username, password string) (string, error) { Password: password, }, &data) if err != nil { - return "", err + return err } s.id = data.ID s.token = data.Token s.name = data.Profile.Name + s.isAdmin = data.Profile.IsAdmin // if Cookie auth, the token will be empty if s.token == "" { - return "", fmt.Errorf("token not found - make sure you are not using Cookie auth on the server") + return fmt.Errorf("token not found - make sure you are not using Cookie auth on the server") } - return data.Token, nil + return nil } func (s *session) destroy() { diff --git a/server/internal/http/legacy/types/types.go b/server/internal/http/legacy/types/types.go index eb05cbac..0fc4b534 100644 --- a/server/internal/http/legacy/types/types.go +++ b/server/internal/http/legacy/types/types.go @@ -1,5 +1,23 @@ package types +import "time" + +type Stats struct { + Connections uint32 `json:"connections"` + Host string `json:"host"` + Members []*Member `json:"members"` + + Banned map[string]string `json:"banned"` // IP -> session ID (that banned it) + Locked map[string]string `json:"locked"` // resource name -> session ID (that locked it) + + ServerStartedAt time.Time `json:"server_started_at"` + LastAdminLeftAt *time.Time `json:"last_admin_left_at"` + LastUserLeftAt *time.Time `json:"last_user_left_at"` + + ControlProtection bool `json:"control_protection"` + ImplicitControl bool `json:"implicit_control"` +} + type Member struct { ID string `json:"id"` Name string `json:"displayname"` diff --git a/server/internal/http/legacy/wstoclient.go b/server/internal/http/legacy/wstoclient.go index 48050616..be5e4c88 100644 --- a/server/internal/http/legacy/wstoclient.go +++ b/server/internal/http/legacy/wstoclient.go @@ -67,6 +67,41 @@ func screenConfigurations(screenSizes []types.ScreenSize) map[int]oldTypes.Scree return screenSizesList } +func (s *session) settingsToLocks(settings types.Settings) (map[string]string, error) { + // + // FileTransfer + // + + filetransferSettings := filetransfer.Settings{ + Enabled: true, // defaults to true + } + + err := settings.Plugins.Unmarshal(filetransfer.PluginName, &filetransferSettings) + if err != nil && !errors.Is(err, types.ErrPluginSettingsNotFound) { + return nil, fmt.Errorf("unable to unmarshal %s plugin settings from global settings: %w", filetransfer.PluginName, err) + } + + // + // Locks + // + + locks := map[string]string{} + if settings.LockedLogins { + locks["login"] = "" // TODO: We don't know who locked the login. + s.lockedLogins = true + } + if settings.LockedControls { + locks["control"] = "" // TODO: We don't know who locked the control. + s.lockedControls = true + } + if !filetransferSettings.Enabled { + locks["file_transfer"] = "" // TODO: We don't know who locked the file transfer. + s.lockedFileTransfer = true + } + + return locks, nil +} + func (s *session) sendControlHost(request message.ControlHost) error { lastHostID := s.lastHostID @@ -198,34 +233,9 @@ func (s *session) wsToClient(msg []byte) error { return err } - // - // FileTransfer - // - - filetransferSettings := filetransfer.Settings{ - Enabled: true, // defaults to true - } - - err = request.Settings.Plugins.Unmarshal(filetransfer.PluginName, &filetransferSettings) - if err != nil && !errors.Is(err, types.ErrPluginSettingsNotFound) { - return fmt.Errorf("unable to unmarshal %s plugin settings from global settings: %w", filetransfer.PluginName, err) - } - - // - // Locks - // - locks := map[string]string{} - if request.Settings.LockedLogins { - locks["login"] = "" // TODO: We don't know who locked the login. - s.lockedLogins = true - } - if request.Settings.LockedControls { - locks["control"] = "" // TODO: We don't know who locked the control. - s.lockedControls = true - } - if !filetransferSettings.Enabled { - locks["file_transfer"] = "" // TODO: We don't know who locked the file transfer. - s.lockedFileTransfer = true + locks, err := s.settingsToLocks(request.Settings) + if err != nil { + return err } return s.toClient(&oldMessage.SystemInit{