/// <summary> /// Returns an existing, or gets a new connection using the details in the specified /// <paramref name="connectToPeerResponse"/> and pierces the remote peer's firewall. /// </summary> /// <remarks> /// This method will be invoked from <see cref="Messaging.Handlers.ServerMessageHandler"/> upon receipt of an /// unsolicited <see cref="ConnectToPeerResponse"/> of type 'P' only. This connection should only be initiated if /// there is no existing connection; superceding should be avoided if possible. /// </remarks> /// <param name="connectToPeerResponse">The response that solicited the connection.</param> /// <returns>The operation context, including the new or updated connection.</returns> public async Task <IMessageConnection> GetOrAddMessageConnectionAsync(ConnectToPeerResponse connectToPeerResponse) { bool cached = true; var r = connectToPeerResponse; try { var connection = await MessageConnectionDictionary.GetOrAdd( r.Username, key => new Lazy <Task <IMessageConnection> >(() => GetConnection())).Value.ConfigureAwait(false); if (cached) { Diagnostic.Debug($"Retrieved cached message connection to {r.Username} ({r.IPEndPoint})"); } return(connection); } catch (Exception ex) { var msg = $"Failed to establish an inbound indirect message connection to {r.Username} ({r.IPEndPoint}): {ex.Message}"; Diagnostic.Debug(msg); Diagnostic.Debug($"Purging message connection cache of failed connection to {r.Username} ({r.IPEndPoint})."); MessageConnectionDictionary.TryRemove(r.Username, out _); throw new ConnectionException(msg, ex); } async Task <IMessageConnection> GetConnection() { cached = false; Diagnostic.Debug($"Attempting inbound indirect message connection to {r.Username} ({r.IPEndPoint}) for token {r.Token}"); var connection = ConnectionFactory.GetMessageConnection( r.Username, r.IPEndPoint, SoulseekClient.Options.PeerConnectionOptions); connection.Type = ConnectionTypes.Inbound | ConnectionTypes.Indirect; connection.MessageRead += SoulseekClient.PeerMessageHandler.HandleMessageRead; connection.MessageReceived += SoulseekClient.PeerMessageHandler.HandleMessageReceived; connection.Disconnected += MessageConnection_Disconnected; try { await connection.ConnectAsync().ConfigureAwait(false); var request = new PierceFirewall(r.Token).ToByteArray(); await connection.WriteAsync(request).ConfigureAwait(false); } catch { connection.Dispose(); throw; } Diagnostic.Debug($"Message connection to {r.Username} ({r.IPEndPoint}) established. (type: {connection.Type}, id: {connection.Id})"); return(connection); } }
private void TryRemoveMessageConnectionRecord(IMessageConnection connection) { if (MessageConnectionDictionary.TryRemove(connection.Username, out _)) { Diagnostic.Debug($"Removed message connection record for {connection.Key.Username} ({connection.IPEndPoint}) (type: {connection.Type}, id: {connection.Id})"); } Diagnostic.Debug($"Message connection cache now contains {MessageConnectionDictionary.Count} connections."); }
/// <summary> /// Removes and disposes all active and queued connections. /// </summary> public async void RemoveAndDisposeAll() { PendingSolicitationDictionary.Clear(); while (!MessageConnectionDictionary.IsEmpty) { if (MessageConnectionDictionary.TryRemove(MessageConnectionDictionary.Keys.First(), out var connection)) { (await connection.Value.ConfigureAwait(false))?.Dispose(); } } }
/// <summary> /// Adds a new message connection from an incoming connection. /// </summary> /// <remarks> /// This method will be invoked from <see cref="ListenerHandler"/> upon receipt of an incoming unsolicited message /// only. Because this connection is fully established by the time it is passed to this method, it must supercede any /// cached connection, as it will be the most recently established connection as tracked by the remote user. /// </remarks> /// <param name="username">The username of the user from which the connection originated.</param> /// <param name="incomingConnection">The the accepted connection.</param> /// <returns>The operation context.</returns> public async Task AddMessageConnectionAsync(string username, IConnection incomingConnection) { var c = incomingConnection; try { await MessageConnectionDictionary.AddOrUpdate( username, new Lazy <Task <IMessageConnection> >(() => GetConnection()), (key, cachedConnectionRecord) => new Lazy <Task <IMessageConnection> >(() => GetConnection(cachedConnectionRecord))).Value.ConfigureAwait(false); } catch (Exception ex) { var msg = $"Failed to establish an inbound message connection to {username} ({c.IPEndPoint}): {ex.Message}"; Diagnostic.Debug($"{msg} (type: {c.Type}, id: {c.Id})"); Diagnostic.Debug($"Purging message connection cache of failed connection to {username} ({c.IPEndPoint})."); MessageConnectionDictionary.TryRemove(username, out _); throw new ConnectionException(msg, ex); } async Task <IMessageConnection> GetConnection(Lazy <Task <IMessageConnection> > cachedConnectionRecord = null) { Diagnostic.Debug($"Inbound message connection to {username} ({c.IPEndPoint}) accepted. (type: {c.Type}, id: {c.Id})"); var connection = ConnectionFactory.GetMessageConnection( username, c.IPEndPoint, SoulseekClient.Options.PeerConnectionOptions, c.HandoffTcpClient()); connection.Type = ConnectionTypes.Inbound | ConnectionTypes.Direct; connection.MessageRead += SoulseekClient.PeerMessageHandler.HandleMessageRead; connection.Disconnected += MessageConnection_Disconnected; Diagnostic.Debug($"Inbound message connection to {username} ({connection.IPEndPoint}) handed off. (old: {c.Id}, new: {connection.Id})"); if (cachedConnectionRecord != null) { var cachedConnection = await cachedConnectionRecord.Value.ConfigureAwait(false); cachedConnection.Disconnected -= MessageConnection_Disconnected; Diagnostic.Debug($"Superceding cached message connection to {username} ({cachedConnection.IPEndPoint}) (old: {cachedConnection.Id}, new: {connection.Id}"); } connection.StartReadingContinuously(); Diagnostic.Debug($"Message connection to {username} ({connection.IPEndPoint}) established. (type: {connection.Type}, id: {connection.Id})"); return(connection); } }
/// <summary> /// Gets a new or existing message connection to the specified <paramref name="username"/>. /// </summary> /// <remarks> /// If a connection doesn't exist, new direct and indirect connections are attempted simultaneously, and the first to /// connect is returned. /// </remarks> /// <param name="username">The username of the user to which to connect.</param> /// <param name="ipEndPoint">The remote IP endpoint of the connection.</param> /// <param name="cancellationToken">The token to monitor for cancellation requests.</param> /// <returns>The operation context, including the new or existing connection.</returns> public async Task <IMessageConnection> GetOrAddMessageConnectionAsync(string username, IPEndPoint ipEndPoint, CancellationToken cancellationToken) { bool cached = true; try { var connection = await MessageConnectionDictionary.GetOrAdd( username, key => new Lazy <Task <IMessageConnection> >(() => GetConnection())).Value.ConfigureAwait(false); if (cached) { Diagnostic.Debug($"Retrieved cached message connection to {username} ({ipEndPoint}) (type: {connection.Type}, id: {connection.Id})"); } return(connection); } catch { Diagnostic.Debug($"Purging message connection cache of failed connection to {username} ({ipEndPoint})."); MessageConnectionDictionary.TryRemove(username, out _); throw; } async Task <IMessageConnection> GetConnection() { cached = false; using var directCts = new CancellationTokenSource(); using var directLinkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, directCts.Token); using var indirectCts = new CancellationTokenSource(); using var indirectLinkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, indirectCts.Token); Diagnostic.Debug($"Attempting simultaneous direct and indirect message connections to {username} ({ipEndPoint})"); var direct = GetMessageConnectionOutboundDirectAsync(username, ipEndPoint, directLinkedCts.Token); var indirect = GetMessageConnectionOutboundIndirectAsync(username, indirectLinkedCts.Token); var tasks = new[] { direct, indirect }.ToList(); Task <IMessageConnection> task; do { task = await Task.WhenAny(tasks).ConfigureAwait(false); tasks.Remove(task); }while (task.Status != TaskStatus.RanToCompletion && tasks.Count > 0); if (task.Status != TaskStatus.RanToCompletion) { var msg = $"Failed to establish a direct or indirect message connection to {username} ({ipEndPoint})"; Diagnostic.Debug(msg); throw new ConnectionException(msg); } var connection = await task.ConfigureAwait(false); connection.Disconnected += MessageConnection_Disconnected; var isDirect = task == direct; Diagnostic.Debug($"{(isDirect ? "Direct" : "Indirect")} message connection to {username} ({ipEndPoint}) established first, attempting to cancel {(isDirect ? "indirect" : "direct")} connection."); (isDirect ? indirectCts : directCts).Cancel(); try { if (isDirect) { var request = new PeerInit(SoulseekClient.Username, Constants.ConnectionType.Peer, SoulseekClient.GetNextToken()); await connection.WriteAsync(request, cancellationToken).ConfigureAwait(false); } else { connection.StartReadingContinuously(); } } catch (Exception ex) { var msg = $"Failed to negotiate message connection to {username} ({ipEndPoint}): {ex.Message}"; Diagnostic.Debug($"{msg} (type: {connection.Type}, id: {connection.Id})"); connection.Dispose(); throw new ConnectionException(msg, ex); } Diagnostic.Debug($"Message connection to {username} ({ipEndPoint}) established. (type: {connection.Type}, id: {connection.Id})"); return(connection); } }
/// <summary> /// Returns an existing, or gets a new connection using the details in the specified /// <paramref name="connectToPeerResponse"/> and pierces the remote peer's firewall. /// </summary> /// <remarks> /// This method will be invoked from <see cref="Messaging.Handlers.ServerMessageHandler"/> upon receipt of an /// unsolicited <see cref="ConnectToPeerResponse"/> of type 'P' only. This connection should only be initiated if /// there is no existing connection; superseding should be avoided if possible. /// </remarks> /// <param name="connectToPeerResponse">The response that solicited the connection.</param> /// <returns>The operation context, including the new or updated connection.</returns> public async Task <IMessageConnection> GetOrAddMessageConnectionAsync(ConnectToPeerResponse connectToPeerResponse) { bool cached = true; var r = connectToPeerResponse; try { var connection = await MessageConnectionDictionary.GetOrAdd( r.Username, key => new Lazy <Task <IMessageConnection> >(() => GetConnection())).Value.ConfigureAwait(false); if (cached) { Diagnostic.Debug($"Retrieved cached message connection to {r.Username} ({r.IPEndPoint}) (type: {connection.Type}, id: {connection.Id})"); } return(connection); } catch (Exception ex) { var msg = $"Failed to establish an inbound indirect message connection to {r.Username} ({r.IPEndPoint}): {ex.Message}"; Diagnostic.Debug(msg); Diagnostic.Debug($"Purging message connection cache of failed connection to {r.Username} ({r.IPEndPoint})."); MessageConnectionDictionary.TryRemove(r.Username, out _); throw new ConnectionException(msg, ex); } async Task <IMessageConnection> GetConnection() { cached = false; Diagnostic.Debug($"Attempting inbound indirect message connection to {r.Username} ({r.IPEndPoint}) for token {r.Token}"); var connection = ConnectionFactory.GetMessageConnection( r.Username, r.IPEndPoint, SoulseekClient.Options.PeerConnectionOptions); connection.Type = ConnectionTypes.Inbound | ConnectionTypes.Indirect; connection.MessageRead += SoulseekClient.PeerMessageHandler.HandleMessageRead; connection.MessageReceived += SoulseekClient.PeerMessageHandler.HandleMessageReceived; connection.MessageWritten += SoulseekClient.PeerMessageHandler.HandleMessageWritten; connection.Disconnected += MessageConnection_Disconnected; using (var cts = new CancellationTokenSource()) { // add a record to the pending dictionary so we can tell whether the following code is waiting PendingInboundIndirectConnectionDictionary.AddOrUpdate(r.Username, cts, (username, existingCts) => cts); try { await connection.ConnectAsync(cts.Token).ConfigureAwait(false); var request = new PierceFirewall(r.Token); await connection.WriteAsync(request, cts.Token).ConfigureAwait(false); } catch { connection.Dispose(); throw; } finally { // let everyone know this code is done executing and that .Value of the containing cache is safe to await with no delay. PendingInboundIndirectConnectionDictionary.TryRemove(r.Username, out _); } } Diagnostic.Debug($"Message connection to {r.Username} ({r.IPEndPoint}) established. (type: {connection.Type}, id: {connection.Id})"); return(connection); } }
/// <summary> /// Adds a new message connection from an incoming connection. /// </summary> /// <remarks> /// This method will be invoked from <see cref="ListenerHandler"/> upon receipt of an incoming unsolicited message /// only. Because this connection is fully established by the time it is passed to this method, it must supersede any /// cached connection, as it will be the most recently established connection as tracked by the remote user. /// </remarks> /// <param name="username">The username of the user from which the connection originated.</param> /// <param name="incomingConnection">The the accepted connection.</param> /// <returns>The operation context.</returns> public async Task AddMessageConnectionAsync(string username, IConnection incomingConnection) { var c = incomingConnection; try { await MessageConnectionDictionary.AddOrUpdate( username, new Lazy <Task <IMessageConnection> >(() => GetConnection()), (key, cachedConnectionRecord) => new Lazy <Task <IMessageConnection> >(() => GetConnection(cachedConnectionRecord))).Value.ConfigureAwait(false); } catch (Exception ex) { var msg = $"Failed to establish an inbound message connection to {username} ({c.IPEndPoint}): {ex.Message}"; Diagnostic.Debug($"{msg} (type: {c.Type}, id: {c.Id})"); Diagnostic.Debug($"Purging message connection cache of failed connection to {username} ({c.IPEndPoint})."); MessageConnectionDictionary.TryRemove(username, out _); throw new ConnectionException(msg, ex); } async Task <IMessageConnection> GetConnection(Lazy <Task <IMessageConnection> > cachedConnectionRecord = null) { Diagnostic.Debug($"Inbound message connection to {username} ({c.IPEndPoint}) accepted. (type: {c.Type}, id: {c.Id})"); var connection = ConnectionFactory.GetMessageConnection( username, c.IPEndPoint, SoulseekClient.Options.PeerConnectionOptions, c.HandoffTcpClient()); connection.Type = ConnectionTypes.Inbound | ConnectionTypes.Direct; connection.MessageRead += SoulseekClient.PeerMessageHandler.HandleMessageRead; connection.MessageWritten += SoulseekClient.PeerMessageHandler.HandleMessageWritten; connection.Disconnected += MessageConnection_Disconnected; Diagnostic.Debug($"Inbound message connection to {username} ({connection.IPEndPoint}) handed off. (old: {c.Id}, new: {connection.Id})"); if (cachedConnectionRecord != null) { // because the cache is Lazy<>, the cached entry may be either a connected or pending connection. // if we try to reference .Value before the cached function is dispositioned we'll get stuck waiting for it, // which will prevent this code from superseding the connection until the pending connection times out. // to get around this the pending connection dictionary was added, allowing us to tell if the connection is still pending. // if so, we can just cancel the token and move on. if (PendingInboundIndirectConnectionDictionary.TryGetValue(username, out var pendingCts)) { Diagnostic.Debug($"Cancelling pending inbound indirect message connection to {username}"); pendingCts.Cancel(); } else { // if there's no entry in the pending connection dictionary, the Lazy<> function has completed executing and we know that // awaiting .Value will return immediately, allowing us to tear down the disconnected event handler. try { var cachedConnection = await cachedConnectionRecord.Value.ConfigureAwait(false); cachedConnection.Disconnected -= MessageConnection_Disconnected; Diagnostic.Debug($"Superseding cached message connection to {username} ({cachedConnection.IPEndPoint}) (old: {cachedConnection.Id}, new: {connection.Id}"); } catch { // noop } } } connection.StartReadingContinuously(); Diagnostic.Debug($"Message connection to {username} ({connection.IPEndPoint}) established. (type: {connection.Type}, id: {connection.Id})"); return(connection); } }
/// <summary> /// Invalidates the cached message connection to the specified <paramref name="username"/>, if one exists. /// </summary> /// <param name="username">The username of the user for which the connection should be removed.</param> /// <returns>A value indicating whether a connection record was invalidated.</returns> public bool TryInvalidateMessageConnectionCache(string username) { return(MessageConnectionDictionary.TryRemove(username, out _)); }
/// <summary> /// Returns an existing, or gets a new connection using the details in the specified /// <paramref name="connectToPeerResponse"/> and pierces the remote peer's firewall. /// </summary> /// <remarks> /// This method will be invoked from <see cref="Messaging.Handlers.ServerMessageHandler"/> upon receipt of an /// unsolicited <see cref="ConnectToPeerResponse"/> of type 'P' only. This connection should only be initiated if /// there is no existing connection; superseding should be avoided if possible. /// </remarks> /// <param name="connectToPeerResponse">The response that solicited the connection.</param> /// <returns>The operation context, including the new or updated connection.</returns> public async Task <IMessageConnection> GetOrAddMessageConnectionAsync(ConnectToPeerResponse connectToPeerResponse) { bool cached = true; var r = connectToPeerResponse; try { var connection = await MessageConnectionDictionary.GetOrAdd( r.Username, key => new Lazy <Task <IMessageConnection> >(() => GetConnection())).Value.ConfigureAwait(false); if (cached) { Diagnostic.Debug($"Retrieved cached message connection to {r.Username} ({r.IPEndPoint}) (type: {connection.Type}, id: {connection.Id})"); } return(connection); } catch (Exception ex) { var msg = $"Failed to establish an inbound indirect message connection to {r.Username} ({r.IPEndPoint}): {ex.Message}"; Diagnostic.Debug(msg); // only purge the connection if the thrown exception is something other than OperationCanceledException. if this // is thrown then a direct connection superseded this connection while it was being established, and // ChildConnectionDictionary contains the new, direct connection. if (!(ex is OperationCanceledException)) { Diagnostic.Debug($"Purging message connection cache of failed connection to {r.Username} ({r.IPEndPoint})."); // remove the current record, which *should* be the one we added above. MessageConnectionDictionary.TryRemove(r.Username, out var removed); try { var connection = await removed.Value.ConfigureAwait(false); // if the connection we removed is Direct, then a direct connection managed to come in after this attempt // had timed out or failed, but before that connection was able to cancel the pending token this should be // an extreme edge case, but log it as a warning so we can see how common it is. if (connection.Type.HasFlag(ConnectionTypes.Direct)) { Diagnostic.Warning($"Erroneously purged direct message connection to {r.Username} upon indirect failure"); MessageConnectionDictionary.TryAdd(r.Username, removed); } } catch { // noop } } throw new ConnectionException(msg, ex); } async Task <IMessageConnection> GetConnection() { cached = false; Diagnostic.Debug($"Attempting inbound indirect message connection to {r.Username} ({r.IPEndPoint}) for token {r.Token}"); var connection = ConnectionFactory.GetMessageConnection( r.Username, r.IPEndPoint, SoulseekClient.Options.PeerConnectionOptions); connection.Type = ConnectionTypes.Inbound | ConnectionTypes.Indirect; connection.MessageRead += SoulseekClient.PeerMessageHandler.HandleMessageRead; connection.MessageReceived += SoulseekClient.PeerMessageHandler.HandleMessageReceived; connection.MessageWritten += SoulseekClient.PeerMessageHandler.HandleMessageWritten; using (var cts = new CancellationTokenSource()) { // add a record to the pending dictionary so we can tell whether the following code is waiting PendingInboundIndirectConnectionDictionary.AddOrUpdate(r.Username, cts, (username, existingCts) => cts); try { await connection.ConnectAsync(cts.Token).ConfigureAwait(false); var request = new PierceFirewall(r.Token); await connection.WriteAsync(request.ToByteArray(), cts.Token).ConfigureAwait(false); } catch { connection.Dispose(); throw; } finally { // let everyone know this code is done executing and that .Value of the containing cache is safe to await // with no delay. PendingInboundIndirectConnectionDictionary.TryRemove(r.Username, out _); } } connection.Disconnected += MessageConnection_Disconnected; Diagnostic.Debug($"Message connection to {r.Username} ({r.IPEndPoint}) established. (type: {connection.Type}, id: {connection.Id})"); return(connection); } }
/// <summary> /// Adds a new message connection from an incoming connection. /// </summary> /// <remarks> /// This method will be invoked from <see cref="ListenerHandler"/> upon receipt of an incoming 'P' connection only. /// Because this connection is fully established by the time it is passed to this method, it must supersede any cached /// connection, as it will be the most recently established connection as tracked by the remote user. /// </remarks> /// <param name="username">The username of the user from which the connection originated.</param> /// <param name="incomingConnection">The the accepted connection.</param> /// <returns>The operation context.</returns> public async Task AddOrUpdateMessageConnectionAsync(string username, IConnection incomingConnection) { var c = incomingConnection; try { await MessageConnectionDictionary.AddOrUpdate( username, new Lazy <Task <IMessageConnection> >(() => GetConnection()), (key, cachedConnectionRecord) => new Lazy <Task <IMessageConnection> >(() => GetConnection(cachedConnectionRecord))).Value.ConfigureAwait(false); } catch (Exception ex) { var msg = $"Failed to establish an inbound message connection to {username} ({c.IPEndPoint}): {ex.Message}"; Diagnostic.Debug($"{msg} (type: {c.Type}, id: {c.Id})"); Diagnostic.Debug($"Purging message connection cache of failed connection to {username} ({c.IPEndPoint})."); MessageConnectionDictionary.TryRemove(username, out _); throw new ConnectionException(msg, ex); } async Task <IMessageConnection> GetConnection(Lazy <Task <IMessageConnection> > cachedConnectionRecord = null) { Diagnostic.Debug($"Inbound message connection to {username} ({c.IPEndPoint}) accepted. (type: {c.Type}, id: {c.Id})"); var connection = ConnectionFactory.GetMessageConnection( username, c.IPEndPoint, SoulseekClient.Options.PeerConnectionOptions, c.HandoffTcpClient()); Diagnostic.Debug($"Inbound message connection to {username} ({connection.IPEndPoint}) handed off. (old: {c.Id}, new: {connection.Id})"); c.Dispose(); connection.Type = ConnectionTypes.Inbound | ConnectionTypes.Direct; connection.MessageRead += SoulseekClient.PeerMessageHandler.HandleMessageRead; connection.MessageReceived += SoulseekClient.PeerMessageHandler.HandleMessageReceived; connection.MessageWritten += SoulseekClient.PeerMessageHandler.HandleMessageWritten; connection.Disconnected += MessageConnection_Disconnected; if (cachedConnectionRecord != null) { if (PendingInboundIndirectConnectionDictionary.TryGetValue(username, out var pendingCts)) { // cancel any connection pending due to a ConnectToPeer message; we don't want it to succeed because the // remote client would supersede this connection with it. Diagnostic.Debug($"Cancelling pending inbound indirect message connection to {username}"); pendingCts.Cancel(); } try { // because we cancelled any pending connection above, the Lazy<> function has completed executing and we // know that awaiting .Value will return immediately, allowing us to tear down the existing connection. var cachedConnection = await cachedConnectionRecord.Value.ConfigureAwait(false); cachedConnection.Disconnected -= MessageConnection_Disconnected; Diagnostic.Debug($"Superseding cached message connection to {username} ({cachedConnection.IPEndPoint}) (old: {cachedConnection.Id}, new: {connection.Id}"); } catch { // noop } } try { connection.StartReadingContinuously(); } catch { connection.Dispose(); throw; } Diagnostic.Debug($"Message connection to {username} ({connection.IPEndPoint}) established. (type: {connection.Type}, id: {connection.Id})"); return(connection); } }