webapi_oscar_bridge.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. package state
  2. import (
  3. "context"
  4. "database/sql"
  5. "encoding/hex"
  6. "errors"
  7. "fmt"
  8. "time"
  9. )
  10. // OSCARBridgeStore manages the persistence of OSCAR bridge sessions in the database.
  11. // It provides methods to store, retrieve, and manage the mapping between WebAPI
  12. // sessions and OSCAR authentication cookies.
  13. type OSCARBridgeStore struct {
  14. store *SQLiteUserStore
  15. }
  16. // NewOSCARBridgeStore creates a new OSCAR bridge store instance.
  17. func (s *SQLiteUserStore) NewOSCARBridgeStore() *OSCARBridgeStore {
  18. return &OSCARBridgeStore{store: s}
  19. }
  20. // OSCARBridgeSession represents a bridge between WebAPI and OSCAR sessions.
  21. type OSCARBridgeSession struct {
  22. WebSessionID string // WebAPI session identifier
  23. OSCARCookie []byte // OSCAR authentication cookie
  24. BOSHost string // BOS server hostname
  25. BOSPort int // BOS server port
  26. UseSSL bool // Whether to use SSL connection
  27. ScreenName string // Screen name associated with the session
  28. ClientName string // Client application name
  29. ClientVersion string // Client application version
  30. CreatedAt time.Time // Bridge creation timestamp
  31. LastAccessed time.Time // Last access timestamp
  32. }
  33. // SaveBridgeSession stores the mapping between WebAPI and OSCAR sessions.
  34. func (s *OSCARBridgeStore) SaveBridgeSession(ctx context.Context, webSessionID string,
  35. oscarCookie []byte, bosHost string, bosPort int) error {
  36. query := `
  37. INSERT INTO oscar_bridge_sessions
  38. (web_session_id, oscar_cookie, bos_host, bos_port, screen_name, created_at, last_accessed)
  39. VALUES (?, ?, ?, ?, ?, ?, ?)
  40. ON CONFLICT(web_session_id) DO UPDATE SET
  41. oscar_cookie = excluded.oscar_cookie,
  42. bos_host = excluded.bos_host,
  43. bos_port = excluded.bos_port,
  44. last_accessed = excluded.last_accessed
  45. `
  46. now := time.Now()
  47. // Note: We'll need to get the screen name from the session manager
  48. // For now, using a placeholder
  49. screenName := "" // This should be passed from the handler
  50. _, err := s.store.db.ExecContext(ctx, query,
  51. webSessionID, oscarCookie, bosHost, bosPort, screenName, now, now)
  52. if err != nil {
  53. return fmt.Errorf("failed to save bridge session: %w", err)
  54. }
  55. return nil
  56. }
  57. // SaveBridgeSessionWithDetails stores a complete bridge session with all details.
  58. func (s *OSCARBridgeStore) SaveBridgeSessionWithDetails(ctx context.Context, session *OSCARBridgeSession) error {
  59. query := `
  60. INSERT INTO oscar_bridge_sessions
  61. (web_session_id, oscar_cookie, bos_host, bos_port, use_ssl, screen_name,
  62. client_name, client_version, created_at, last_accessed)
  63. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  64. ON CONFLICT(web_session_id) DO UPDATE SET
  65. oscar_cookie = excluded.oscar_cookie,
  66. bos_host = excluded.bos_host,
  67. bos_port = excluded.bos_port,
  68. use_ssl = excluded.use_ssl,
  69. last_accessed = excluded.last_accessed
  70. `
  71. _, err := s.store.db.ExecContext(ctx, query,
  72. session.WebSessionID, session.OSCARCookie, session.BOSHost, session.BOSPort,
  73. session.UseSSL, session.ScreenName, session.ClientName, session.ClientVersion,
  74. session.CreatedAt, session.LastAccessed)
  75. if err != nil {
  76. return fmt.Errorf("failed to save bridge session: %w", err)
  77. }
  78. return nil
  79. }
  80. // GetBridgeSession retrieves bridge session details by WebAPI session ID.
  81. func (s *OSCARBridgeStore) GetBridgeSession(ctx context.Context, webSessionID string) (*OSCARBridgeSession, error) {
  82. query := `
  83. SELECT web_session_id, oscar_cookie, bos_host, bos_port, use_ssl, screen_name,
  84. client_name, client_version, created_at, last_accessed
  85. FROM oscar_bridge_sessions
  86. WHERE web_session_id = ?
  87. `
  88. var session OSCARBridgeSession
  89. var clientName, clientVersion sql.NullString
  90. err := s.store.db.QueryRowContext(ctx, query, webSessionID).Scan(
  91. &session.WebSessionID,
  92. &session.OSCARCookie,
  93. &session.BOSHost,
  94. &session.BOSPort,
  95. &session.UseSSL,
  96. &session.ScreenName,
  97. &clientName,
  98. &clientVersion,
  99. &session.CreatedAt,
  100. &session.LastAccessed,
  101. )
  102. if err != nil {
  103. if errors.Is(err, sql.ErrNoRows) {
  104. return nil, fmt.Errorf("bridge session not found")
  105. }
  106. return nil, fmt.Errorf("failed to get bridge session: %w", err)
  107. }
  108. // Handle nullable fields
  109. if clientName.Valid {
  110. session.ClientName = clientName.String
  111. }
  112. if clientVersion.Valid {
  113. session.ClientVersion = clientVersion.String
  114. }
  115. // Update last accessed time
  116. go s.touchSession(context.Background(), webSessionID)
  117. return &session, nil
  118. }
  119. // GetBridgeSessionByScreenName retrieves bridge sessions by screen name.
  120. func (s *OSCARBridgeStore) GetBridgeSessionByScreenName(ctx context.Context, screenName string) ([]*OSCARBridgeSession, error) {
  121. query := `
  122. SELECT web_session_id, oscar_cookie, bos_host, bos_port, use_ssl, screen_name,
  123. client_name, client_version, created_at, last_accessed
  124. FROM oscar_bridge_sessions
  125. WHERE screen_name = ?
  126. ORDER BY last_accessed DESC
  127. `
  128. rows, err := s.store.db.QueryContext(ctx, query, screenName)
  129. if err != nil {
  130. return nil, fmt.Errorf("failed to query bridge sessions: %w", err)
  131. }
  132. defer rows.Close()
  133. var sessions []*OSCARBridgeSession
  134. for rows.Next() {
  135. var session OSCARBridgeSession
  136. var clientName, clientVersion sql.NullString
  137. err := rows.Scan(
  138. &session.WebSessionID,
  139. &session.OSCARCookie,
  140. &session.BOSHost,
  141. &session.BOSPort,
  142. &session.UseSSL,
  143. &session.ScreenName,
  144. &clientName,
  145. &clientVersion,
  146. &session.CreatedAt,
  147. &session.LastAccessed,
  148. )
  149. if err != nil {
  150. return nil, fmt.Errorf("failed to scan bridge session: %w", err)
  151. }
  152. // Handle nullable fields
  153. if clientName.Valid {
  154. session.ClientName = clientName.String
  155. }
  156. if clientVersion.Valid {
  157. session.ClientVersion = clientVersion.String
  158. }
  159. sessions = append(sessions, &session)
  160. }
  161. if err := rows.Err(); err != nil {
  162. return nil, fmt.Errorf("error iterating bridge sessions: %w", err)
  163. }
  164. return sessions, nil
  165. }
  166. // DeleteBridgeSession removes a bridge session.
  167. func (s *OSCARBridgeStore) DeleteBridgeSession(ctx context.Context, webSessionID string) error {
  168. query := `DELETE FROM oscar_bridge_sessions WHERE web_session_id = ?`
  169. result, err := s.store.db.ExecContext(ctx, query, webSessionID)
  170. if err != nil {
  171. return fmt.Errorf("failed to delete bridge session: %w", err)
  172. }
  173. rowsAffected, err := result.RowsAffected()
  174. if err != nil {
  175. return fmt.Errorf("failed to get rows affected: %w", err)
  176. }
  177. if rowsAffected == 0 {
  178. return fmt.Errorf("bridge session not found")
  179. }
  180. return nil
  181. }
  182. // CleanupExpiredSessions removes bridge sessions that haven't been accessed recently.
  183. func (s *OSCARBridgeStore) CleanupExpiredSessions(ctx context.Context, maxAge time.Duration) (int, error) {
  184. cutoff := time.Now().Add(-maxAge)
  185. query := `DELETE FROM oscar_bridge_sessions WHERE last_accessed < ?`
  186. result, err := s.store.db.ExecContext(ctx, query, cutoff)
  187. if err != nil {
  188. return 0, fmt.Errorf("failed to cleanup expired sessions: %w", err)
  189. }
  190. rowsAffected, err := result.RowsAffected()
  191. if err != nil {
  192. return 0, fmt.Errorf("failed to get rows affected: %w", err)
  193. }
  194. return int(rowsAffected), nil
  195. }
  196. // touchSession updates the last accessed time for a session (internal helper).
  197. func (s *OSCARBridgeStore) touchSession(ctx context.Context, webSessionID string) {
  198. query := `UPDATE oscar_bridge_sessions SET last_accessed = ? WHERE web_session_id = ?`
  199. s.store.db.ExecContext(ctx, query, time.Now(), webSessionID)
  200. }
  201. // GetAllBridgeSessions returns all active bridge sessions (for monitoring/admin).
  202. func (s *OSCARBridgeStore) GetAllBridgeSessions(ctx context.Context) ([]*OSCARBridgeSession, error) {
  203. query := `
  204. SELECT web_session_id, oscar_cookie, bos_host, bos_port, use_ssl, screen_name,
  205. client_name, client_version, created_at, last_accessed
  206. FROM oscar_bridge_sessions
  207. ORDER BY last_accessed DESC
  208. `
  209. rows, err := s.store.db.QueryContext(ctx, query)
  210. if err != nil {
  211. return nil, fmt.Errorf("failed to query all bridge sessions: %w", err)
  212. }
  213. defer rows.Close()
  214. var sessions []*OSCARBridgeSession
  215. for rows.Next() {
  216. var session OSCARBridgeSession
  217. var clientName, clientVersion sql.NullString
  218. err := rows.Scan(
  219. &session.WebSessionID,
  220. &session.OSCARCookie,
  221. &session.BOSHost,
  222. &session.BOSPort,
  223. &session.UseSSL,
  224. &session.ScreenName,
  225. &clientName,
  226. &clientVersion,
  227. &session.CreatedAt,
  228. &session.LastAccessed,
  229. )
  230. if err != nil {
  231. return nil, fmt.Errorf("failed to scan bridge session: %w", err)
  232. }
  233. // Handle nullable fields
  234. if clientName.Valid {
  235. session.ClientName = clientName.String
  236. }
  237. if clientVersion.Valid {
  238. session.ClientVersion = clientVersion.String
  239. }
  240. sessions = append(sessions, &session)
  241. }
  242. if err := rows.Err(); err != nil {
  243. return nil, fmt.Errorf("error iterating bridge sessions: %w", err)
  244. }
  245. return sessions, nil
  246. }
  247. // GetStatistics returns statistics about bridge sessions.
  248. func (s *OSCARBridgeStore) GetStatistics(ctx context.Context) (map[string]interface{}, error) {
  249. stats := make(map[string]interface{})
  250. // Total sessions
  251. var totalCount int
  252. err := s.store.db.QueryRowContext(ctx,
  253. `SELECT COUNT(*) FROM oscar_bridge_sessions`).Scan(&totalCount)
  254. if err != nil {
  255. return nil, fmt.Errorf("failed to get total count: %w", err)
  256. }
  257. stats["total_sessions"] = totalCount
  258. // Active sessions (accessed in last hour)
  259. var activeCount int
  260. oneHourAgo := time.Now().Add(-time.Hour)
  261. err = s.store.db.QueryRowContext(ctx,
  262. `SELECT COUNT(*) FROM oscar_bridge_sessions WHERE last_accessed > ?`,
  263. oneHourAgo).Scan(&activeCount)
  264. if err != nil {
  265. return nil, fmt.Errorf("failed to get active count: %w", err)
  266. }
  267. stats["active_sessions"] = activeCount
  268. // SSL vs non-SSL
  269. var sslCount int
  270. err = s.store.db.QueryRowContext(ctx,
  271. `SELECT COUNT(*) FROM oscar_bridge_sessions WHERE use_ssl = true`).Scan(&sslCount)
  272. if err != nil {
  273. return nil, fmt.Errorf("failed to get SSL count: %w", err)
  274. }
  275. stats["ssl_sessions"] = sslCount
  276. stats["non_ssl_sessions"] = totalCount - sslCount
  277. return stats, nil
  278. }
  279. // ValidateOSCARCookie checks if an OSCAR cookie exists in the bridge store.
  280. // This can be used to validate incoming OSCAR connections.
  281. func (s *OSCARBridgeStore) ValidateOSCARCookie(ctx context.Context, cookie []byte) (*OSCARBridgeSession, error) {
  282. // Convert cookie to hex for comparison
  283. cookieHex := hex.EncodeToString(cookie)
  284. query := `
  285. SELECT web_session_id, oscar_cookie, bos_host, bos_port, use_ssl, screen_name,
  286. client_name, client_version, created_at, last_accessed
  287. FROM oscar_bridge_sessions
  288. WHERE hex(oscar_cookie) = ?
  289. `
  290. var session OSCARBridgeSession
  291. var clientName, clientVersion sql.NullString
  292. err := s.store.db.QueryRowContext(ctx, query, cookieHex).Scan(
  293. &session.WebSessionID,
  294. &session.OSCARCookie,
  295. &session.BOSHost,
  296. &session.BOSPort,
  297. &session.UseSSL,
  298. &session.ScreenName,
  299. &clientName,
  300. &clientVersion,
  301. &session.CreatedAt,
  302. &session.LastAccessed,
  303. )
  304. if err != nil {
  305. if errors.Is(err, sql.ErrNoRows) {
  306. return nil, fmt.Errorf("cookie not found")
  307. }
  308. return nil, fmt.Errorf("failed to validate cookie: %w", err)
  309. }
  310. // Handle nullable fields
  311. if clientName.Valid {
  312. session.ClientName = clientName.String
  313. }
  314. if clientVersion.Valid {
  315. session.ClientVersion = clientVersion.String
  316. }
  317. // Update last accessed time
  318. go s.touchSession(context.Background(), session.WebSessionID)
  319. return &session, nil
  320. }