| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383 |
- package state
- import (
- "context"
- "database/sql"
- "encoding/hex"
- "errors"
- "fmt"
- "time"
- )
- // OSCARBridgeStore manages the persistence of OSCAR bridge sessions in the database.
- // It provides methods to store, retrieve, and manage the mapping between WebAPI
- // sessions and OSCAR authentication cookies.
- type OSCARBridgeStore struct {
- store *SQLiteUserStore
- }
- // NewOSCARBridgeStore creates a new OSCAR bridge store instance.
- func (s *SQLiteUserStore) NewOSCARBridgeStore() *OSCARBridgeStore {
- return &OSCARBridgeStore{store: s}
- }
- // OSCARBridgeSession represents a bridge between WebAPI and OSCAR sessions.
- type OSCARBridgeSession struct {
- WebSessionID string // WebAPI session identifier
- OSCARCookie []byte // OSCAR authentication cookie
- BOSHost string // BOS server hostname
- BOSPort int // BOS server port
- UseSSL bool // Whether to use SSL connection
- ScreenName string // Screen name associated with the session
- ClientName string // Client application name
- ClientVersion string // Client application version
- CreatedAt time.Time // Bridge creation timestamp
- LastAccessed time.Time // Last access timestamp
- }
- // SaveBridgeSession stores the mapping between WebAPI and OSCAR sessions.
- func (s *OSCARBridgeStore) SaveBridgeSession(ctx context.Context, webSessionID string,
- oscarCookie []byte, bosHost string, bosPort int) error {
- query := `
- INSERT INTO oscar_bridge_sessions
- (web_session_id, oscar_cookie, bos_host, bos_port, screen_name, created_at, last_accessed)
- VALUES (?, ?, ?, ?, ?, ?, ?)
- ON CONFLICT(web_session_id) DO UPDATE SET
- oscar_cookie = excluded.oscar_cookie,
- bos_host = excluded.bos_host,
- bos_port = excluded.bos_port,
- last_accessed = excluded.last_accessed
- `
- now := time.Now()
- // Note: We'll need to get the screen name from the session manager
- // For now, using a placeholder
- screenName := "" // This should be passed from the handler
- _, err := s.store.db.ExecContext(ctx, query,
- webSessionID, oscarCookie, bosHost, bosPort, screenName, now, now)
- if err != nil {
- return fmt.Errorf("failed to save bridge session: %w", err)
- }
- return nil
- }
- // SaveBridgeSessionWithDetails stores a complete bridge session with all details.
- func (s *OSCARBridgeStore) SaveBridgeSessionWithDetails(ctx context.Context, session *OSCARBridgeSession) error {
- query := `
- INSERT INTO oscar_bridge_sessions
- (web_session_id, oscar_cookie, bos_host, bos_port, use_ssl, screen_name,
- client_name, client_version, created_at, last_accessed)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- ON CONFLICT(web_session_id) DO UPDATE SET
- oscar_cookie = excluded.oscar_cookie,
- bos_host = excluded.bos_host,
- bos_port = excluded.bos_port,
- use_ssl = excluded.use_ssl,
- last_accessed = excluded.last_accessed
- `
- _, err := s.store.db.ExecContext(ctx, query,
- session.WebSessionID, session.OSCARCookie, session.BOSHost, session.BOSPort,
- session.UseSSL, session.ScreenName, session.ClientName, session.ClientVersion,
- session.CreatedAt, session.LastAccessed)
- if err != nil {
- return fmt.Errorf("failed to save bridge session: %w", err)
- }
- return nil
- }
- // GetBridgeSession retrieves bridge session details by WebAPI session ID.
- func (s *OSCARBridgeStore) GetBridgeSession(ctx context.Context, webSessionID string) (*OSCARBridgeSession, error) {
- query := `
- SELECT web_session_id, oscar_cookie, bos_host, bos_port, use_ssl, screen_name,
- client_name, client_version, created_at, last_accessed
- FROM oscar_bridge_sessions
- WHERE web_session_id = ?
- `
- var session OSCARBridgeSession
- var clientName, clientVersion sql.NullString
- err := s.store.db.QueryRowContext(ctx, query, webSessionID).Scan(
- &session.WebSessionID,
- &session.OSCARCookie,
- &session.BOSHost,
- &session.BOSPort,
- &session.UseSSL,
- &session.ScreenName,
- &clientName,
- &clientVersion,
- &session.CreatedAt,
- &session.LastAccessed,
- )
- if err != nil {
- if errors.Is(err, sql.ErrNoRows) {
- return nil, fmt.Errorf("bridge session not found")
- }
- return nil, fmt.Errorf("failed to get bridge session: %w", err)
- }
- // Handle nullable fields
- if clientName.Valid {
- session.ClientName = clientName.String
- }
- if clientVersion.Valid {
- session.ClientVersion = clientVersion.String
- }
- // Update last accessed time
- go s.touchSession(context.Background(), webSessionID)
- return &session, nil
- }
- // GetBridgeSessionByScreenName retrieves bridge sessions by screen name.
- func (s *OSCARBridgeStore) GetBridgeSessionByScreenName(ctx context.Context, screenName string) ([]*OSCARBridgeSession, error) {
- query := `
- SELECT web_session_id, oscar_cookie, bos_host, bos_port, use_ssl, screen_name,
- client_name, client_version, created_at, last_accessed
- FROM oscar_bridge_sessions
- WHERE screen_name = ?
- ORDER BY last_accessed DESC
- `
- rows, err := s.store.db.QueryContext(ctx, query, screenName)
- if err != nil {
- return nil, fmt.Errorf("failed to query bridge sessions: %w", err)
- }
- defer rows.Close()
- var sessions []*OSCARBridgeSession
- for rows.Next() {
- var session OSCARBridgeSession
- var clientName, clientVersion sql.NullString
- err := rows.Scan(
- &session.WebSessionID,
- &session.OSCARCookie,
- &session.BOSHost,
- &session.BOSPort,
- &session.UseSSL,
- &session.ScreenName,
- &clientName,
- &clientVersion,
- &session.CreatedAt,
- &session.LastAccessed,
- )
- if err != nil {
- return nil, fmt.Errorf("failed to scan bridge session: %w", err)
- }
- // Handle nullable fields
- if clientName.Valid {
- session.ClientName = clientName.String
- }
- if clientVersion.Valid {
- session.ClientVersion = clientVersion.String
- }
- sessions = append(sessions, &session)
- }
- if err := rows.Err(); err != nil {
- return nil, fmt.Errorf("error iterating bridge sessions: %w", err)
- }
- return sessions, nil
- }
- // DeleteBridgeSession removes a bridge session.
- func (s *OSCARBridgeStore) DeleteBridgeSession(ctx context.Context, webSessionID string) error {
- query := `DELETE FROM oscar_bridge_sessions WHERE web_session_id = ?`
- result, err := s.store.db.ExecContext(ctx, query, webSessionID)
- if err != nil {
- return fmt.Errorf("failed to delete bridge session: %w", err)
- }
- rowsAffected, err := result.RowsAffected()
- if err != nil {
- return fmt.Errorf("failed to get rows affected: %w", err)
- }
- if rowsAffected == 0 {
- return fmt.Errorf("bridge session not found")
- }
- return nil
- }
- // CleanupExpiredSessions removes bridge sessions that haven't been accessed recently.
- func (s *OSCARBridgeStore) CleanupExpiredSessions(ctx context.Context, maxAge time.Duration) (int, error) {
- cutoff := time.Now().Add(-maxAge)
- query := `DELETE FROM oscar_bridge_sessions WHERE last_accessed < ?`
- result, err := s.store.db.ExecContext(ctx, query, cutoff)
- if err != nil {
- return 0, fmt.Errorf("failed to cleanup expired sessions: %w", err)
- }
- rowsAffected, err := result.RowsAffected()
- if err != nil {
- return 0, fmt.Errorf("failed to get rows affected: %w", err)
- }
- return int(rowsAffected), nil
- }
- // touchSession updates the last accessed time for a session (internal helper).
- func (s *OSCARBridgeStore) touchSession(ctx context.Context, webSessionID string) {
- query := `UPDATE oscar_bridge_sessions SET last_accessed = ? WHERE web_session_id = ?`
- s.store.db.ExecContext(ctx, query, time.Now(), webSessionID)
- }
- // GetAllBridgeSessions returns all active bridge sessions (for monitoring/admin).
- func (s *OSCARBridgeStore) GetAllBridgeSessions(ctx context.Context) ([]*OSCARBridgeSession, error) {
- query := `
- SELECT web_session_id, oscar_cookie, bos_host, bos_port, use_ssl, screen_name,
- client_name, client_version, created_at, last_accessed
- FROM oscar_bridge_sessions
- ORDER BY last_accessed DESC
- `
- rows, err := s.store.db.QueryContext(ctx, query)
- if err != nil {
- return nil, fmt.Errorf("failed to query all bridge sessions: %w", err)
- }
- defer rows.Close()
- var sessions []*OSCARBridgeSession
- for rows.Next() {
- var session OSCARBridgeSession
- var clientName, clientVersion sql.NullString
- err := rows.Scan(
- &session.WebSessionID,
- &session.OSCARCookie,
- &session.BOSHost,
- &session.BOSPort,
- &session.UseSSL,
- &session.ScreenName,
- &clientName,
- &clientVersion,
- &session.CreatedAt,
- &session.LastAccessed,
- )
- if err != nil {
- return nil, fmt.Errorf("failed to scan bridge session: %w", err)
- }
- // Handle nullable fields
- if clientName.Valid {
- session.ClientName = clientName.String
- }
- if clientVersion.Valid {
- session.ClientVersion = clientVersion.String
- }
- sessions = append(sessions, &session)
- }
- if err := rows.Err(); err != nil {
- return nil, fmt.Errorf("error iterating bridge sessions: %w", err)
- }
- return sessions, nil
- }
- // GetStatistics returns statistics about bridge sessions.
- func (s *OSCARBridgeStore) GetStatistics(ctx context.Context) (map[string]interface{}, error) {
- stats := make(map[string]interface{})
- // Total sessions
- var totalCount int
- err := s.store.db.QueryRowContext(ctx,
- `SELECT COUNT(*) FROM oscar_bridge_sessions`).Scan(&totalCount)
- if err != nil {
- return nil, fmt.Errorf("failed to get total count: %w", err)
- }
- stats["total_sessions"] = totalCount
- // Active sessions (accessed in last hour)
- var activeCount int
- oneHourAgo := time.Now().Add(-time.Hour)
- err = s.store.db.QueryRowContext(ctx,
- `SELECT COUNT(*) FROM oscar_bridge_sessions WHERE last_accessed > ?`,
- oneHourAgo).Scan(&activeCount)
- if err != nil {
- return nil, fmt.Errorf("failed to get active count: %w", err)
- }
- stats["active_sessions"] = activeCount
- // SSL vs non-SSL
- var sslCount int
- err = s.store.db.QueryRowContext(ctx,
- `SELECT COUNT(*) FROM oscar_bridge_sessions WHERE use_ssl = true`).Scan(&sslCount)
- if err != nil {
- return nil, fmt.Errorf("failed to get SSL count: %w", err)
- }
- stats["ssl_sessions"] = sslCount
- stats["non_ssl_sessions"] = totalCount - sslCount
- return stats, nil
- }
- // ValidateOSCARCookie checks if an OSCAR cookie exists in the bridge store.
- // This can be used to validate incoming OSCAR connections.
- func (s *OSCARBridgeStore) ValidateOSCARCookie(ctx context.Context, cookie []byte) (*OSCARBridgeSession, error) {
- // Convert cookie to hex for comparison
- cookieHex := hex.EncodeToString(cookie)
- query := `
- SELECT web_session_id, oscar_cookie, bos_host, bos_port, use_ssl, screen_name,
- client_name, client_version, created_at, last_accessed
- FROM oscar_bridge_sessions
- WHERE hex(oscar_cookie) = ?
- `
- var session OSCARBridgeSession
- var clientName, clientVersion sql.NullString
- err := s.store.db.QueryRowContext(ctx, query, cookieHex).Scan(
- &session.WebSessionID,
- &session.OSCARCookie,
- &session.BOSHost,
- &session.BOSPort,
- &session.UseSSL,
- &session.ScreenName,
- &clientName,
- &clientVersion,
- &session.CreatedAt,
- &session.LastAccessed,
- )
- if err != nil {
- if errors.Is(err, sql.ErrNoRows) {
- return nil, fmt.Errorf("cookie not found")
- }
- return nil, fmt.Errorf("failed to validate cookie: %w", err)
- }
- // Handle nullable fields
- if clientName.Valid {
- session.ClientName = clientName.String
- }
- if clientVersion.Valid {
- session.ClientVersion = clientVersion.String
- }
- // Update last accessed time
- go s.touchSession(context.Background(), session.WebSessionID)
- return &session, nil
- }
|