// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. part of "dart:_http"; const String _DART_SESSION_ID = "DARTSESSID"; // A _HttpSession is a node in a double-linked list, with _next and _prev being // the previous and next pointers. class _HttpSession implements HttpSession { // Destroyed marked. Used by the http connection to see if a session is valid. bool _destroyed = false; bool _isNew = true; DateTime _lastSeen; void Function()? _timeoutCallback; final _HttpSessionManager _sessionManager; // Pointers in timeout queue. _HttpSession? _prev; _HttpSession? _next; final String id; final Map _data = HashMap(); _HttpSession(this._sessionManager, this.id) : _lastSeen = DateTime.now(); void destroy() { assert(!_destroyed); _destroyed = true; _sessionManager._removeFromTimeoutQueue(this); _sessionManager._sessions.remove(id); } // Mark the session as seen. This will reset the timeout and move the node to // the end of the timeout queue. void _markSeen() { _lastSeen = DateTime.now(); _sessionManager._bumpToEnd(this); } DateTime get lastSeen => _lastSeen; bool get isNew => _isNew; void set onTimeout(void Function()? callback) { _timeoutCallback = callback; } // Map implementation: bool containsValue(value) => _data.containsValue(value); bool containsKey(key) => _data.containsKey(key); operator [](key) => _data[key]; void operator []=(key, value) { _data[key] = value; } putIfAbsent(key, ifAbsent) => _data.putIfAbsent(key, ifAbsent); addAll(Map other) => _data.addAll(other); remove(key) => _data.remove(key); void clear() { _data.clear(); } void forEach(void f(key, value)) { _data.forEach(f); } Iterable get entries => _data.entries; void addEntries(Iterable entries) { _data.addEntries(entries); } Map map(MapEntry transform(key, value)) => _data.map(transform); void removeWhere(bool test(key, value)) { _data.removeWhere(test); } Map cast() => _data.cast(); update(key, update(value), {Function()? ifAbsent}) => _data.update(key, update, ifAbsent: ifAbsent); void updateAll(update(key, value)) { _data.updateAll(update); } Iterable get keys => _data.keys; Iterable get values => _data.values; int get length => _data.length; bool get isEmpty => _data.isEmpty; bool get isNotEmpty => _data.isNotEmpty; String toString() => 'HttpSession id:$id $_data'; } // Private class used to manage all the active sessions. The sessions are stored // in two ways: // // * In a map, mapping from ID to HttpSession. // * In a linked list, used as a timeout queue. class _HttpSessionManager { final Map _sessions; int _sessionTimeout = 20 * 60; // 20 mins. _HttpSession? _head; _HttpSession? _tail; Timer? _timer; _HttpSessionManager() : _sessions = {}; String createSessionId() { const int _KEY_LENGTH = 16; // 128 bits. var data = _CryptoUtils.getRandomBytes(_KEY_LENGTH); return _CryptoUtils.bytesToHex(data); } _HttpSession? getSession(String id) => _sessions[id]; _HttpSession createSession() { var id = createSessionId(); // TODO(ajohnsen): Consider adding a limit and throwing an exception. // Should be very unlikely however. while (_sessions.containsKey(id)) { id = createSessionId(); } var session = _sessions[id] = _HttpSession(this, id); _addToTimeoutQueue(session); return session; } void set sessionTimeout(int timeout) { _sessionTimeout = timeout; _stopTimer(); _startTimer(); } void close() { _stopTimer(); } void _bumpToEnd(_HttpSession session) { _removeFromTimeoutQueue(session); _addToTimeoutQueue(session); } void _addToTimeoutQueue(_HttpSession session) { if (_head == null) { assert(_tail == null); _tail = _head = session; _startTimer(); } else { assert(_timer != null); var tail = _tail!; // Add to end. tail._next = session; session._prev = tail; _tail = session; } } void _removeFromTimeoutQueue(_HttpSession session) { var next = session._next; var prev = session._prev; session._next = session._prev = null; next?._prev = prev; prev?._next = next; if (_tail == session) { _tail = prev; } if (_head == session) { _head = next; // We removed the head element, start new timer. _stopTimer(); _startTimer(); } } void _timerTimeout() { _stopTimer(); // Clear timer. var session = _head!; session.destroy(); // Will remove the session from timeout queue and map. session._timeoutCallback?.call(); } void _startTimer() { assert(_timer == null); var head = _head; if (head != null) { int seconds = DateTime.now().difference(head.lastSeen).inSeconds; _timer = Timer( Duration(seconds: _sessionTimeout - seconds), _timerTimeout, ); } } void _stopTimer() { var timer = _timer; if (timer != null) { timer.cancel(); _timer = null; } } }