#if !BESTHTTP_DISABLE_SIGNALR_CORE
#if CSHARP_7_OR_LATER
using System.Threading.Tasks;
#endif
using BestHTTP.Futures;
using BestHTTP.PlatformSupport.Memory;
using BestHTTP.SignalRCore.Authentication;
using BestHTTP.SignalRCore.Messages;
using System;
using System.Collections.Generic;
namespace BestHTTP.SignalRCore
{
public sealed class HubConnection : BestHTTP.Extensions.IHeartbeat
{
public static readonly object[] EmptyArgs = new object[0];
///
/// Uri of the Hub endpoint
///
public Uri Uri { get; private set; }
///
/// Current state of this connection.
///
public ConnectionStates State { get; private set; }
///
/// Current, active ITransport instance.
///
public ITransport Transport { get; private set; }
///
/// The IProtocol implementation that will parse, encode and decode messages.
///
public IProtocol Protocol { get; private set; }
///
/// This event is called when the connection is redirected to a new uri.
///
public event Action OnRedirected;
///
/// This event is called when successfully connected to the hub.
///
public event Action OnConnected;
///
/// This event is called when an unexpected error happen and the connection is closed.
///
public event Action OnError;
///
/// This event is called when the connection is gracefully terminated.
///
public event Action OnClosed;
///
/// This event is called for every server-sent message. When returns false, no further processing of the message is done by the plugin.
///
public event Func OnMessage;
///
/// Called when the HubConnection start its reconnection process after loosing its underlying connection.
///
public event Action OnReconnecting;
///
/// Called after a succesfull reconnection.
///
public event Action OnReconnected;
///
/// Called for transport related events.
///
public event Action OnTransportEvent;
///
/// An IAuthenticationProvider implementation that will be used to authenticate the connection.
///
public IAuthenticationProvider AuthenticationProvider { get; set; }
///
/// Negotiation response sent by the server.
///
public NegotiationResult NegotiationResult { get; private set; }
///
/// Options that has been used to create the HubConnection.
///
public HubOptions Options { get; private set; }
///
/// How many times this connection is redirected.
///
public int RedirectCount { get; private set; }
///
/// The reconnect policy that will be used when the underlying connection is lost. Its default value is null.
///
public IRetryPolicy ReconnectPolicy { get; set; }
///
/// This will be increment to add a unique id to every message the plugin will send.
///
private long lastInvocationId = 1;
///
/// Id of the last streaming parameter.
///
private int lastStreamId = 1;
///
/// Store the callback for all sent message that expect a return value from the server. All sent message has
/// a unique invocationId that will be sent back from the server.
///
private Dictionary invocations = new Dictionary();
///
/// This is where we store the methodname => callback mapping.
///
private Dictionary subscriptions = new Dictionary(StringComparer.OrdinalIgnoreCase);
///
/// When we sent out the last message to the server.
///
private DateTime lastMessageSent;
private RetryContext currentContext;
private DateTime reconnectStartTime = DateTime.MinValue;
private DateTime reconnectAt;
private List triedoutTransports = new List();
public HubConnection(Uri hubUri, IProtocol protocol)
: this(hubUri, protocol, new HubOptions())
{
}
public HubConnection(Uri hubUri, IProtocol protocol, HubOptions options)
{
this.Uri = hubUri;
this.State = ConnectionStates.Initial;
this.Options = options;
this.Protocol = protocol;
this.Protocol.Connection = this;
this.AuthenticationProvider = new DefaultAccessTokenAuthenticator(this);
}
public void StartConnect()
{
if (this.State != ConnectionStates.Initial && this.State != ConnectionStates.Redirected && this.State != ConnectionStates.Reconnecting)
{
HTTPManager.Logger.Warning("HubConnection", "StartConnect - Expected Initial or Redirected state, got " + this.State.ToString());
return;
}
HTTPManager.Logger.Verbose("HubConnection", "StartConnect");
if (this.AuthenticationProvider != null && this.AuthenticationProvider.IsPreAuthRequired)
{
HTTPManager.Logger.Information("HubConnection", "StartConnect - Authenticating");
SetState(ConnectionStates.Authenticating);
this.AuthenticationProvider.OnAuthenticationSucceded += OnAuthenticationSucceded;
this.AuthenticationProvider.OnAuthenticationFailed += OnAuthenticationFailed;
// Start the authentication process
this.AuthenticationProvider.StartAuthentication();
}
else
StartNegotiation();
}
#if CSHARP_7_OR_LATER
TaskCompletionSource connectAsyncTaskCompletionSource;
public Task ConnectAsync()
{
if (this.State != ConnectionStates.Initial && this.State != ConnectionStates.Redirected && this.State != ConnectionStates.Reconnecting)
throw new Exception("HubConnection - ConnectAsync - Expected Initial or Redirected state, got " + this.State.ToString());
if (this.connectAsyncTaskCompletionSource != null)
throw new Exception("Connect process already started!");
this.connectAsyncTaskCompletionSource = new TaskCompletionSource();
this.OnConnected += OnAsyncConnectedCallback;
this.OnError += OnAsyncConnectFailedCallback;
this.StartConnect();
return connectAsyncTaskCompletionSource.Task;
}
private void OnAsyncConnectedCallback(HubConnection hub)
{
this.OnConnected -= OnAsyncConnectedCallback;
this.OnError -= OnAsyncConnectFailedCallback;
this.connectAsyncTaskCompletionSource.TrySetResult(this);
this.connectAsyncTaskCompletionSource = null;
}
private void OnAsyncConnectFailedCallback(HubConnection hub, string error)
{
this.OnConnected -= OnAsyncConnectedCallback;
this.OnError -= OnAsyncConnectFailedCallback;
this.connectAsyncTaskCompletionSource.TrySetException(new Exception(error));
this.connectAsyncTaskCompletionSource = null;
}
#endif
private void OnAuthenticationSucceded(IAuthenticationProvider provider)
{
HTTPManager.Logger.Verbose("HubConnection", "OnAuthenticationSucceded");
this.AuthenticationProvider.OnAuthenticationSucceded -= OnAuthenticationSucceded;
this.AuthenticationProvider.OnAuthenticationFailed -= OnAuthenticationFailed;
StartNegotiation();
}
private void OnAuthenticationFailed(IAuthenticationProvider provider, string reason)
{
HTTPManager.Logger.Error("HubConnection", "OnAuthenticationFailed: " + reason);
this.AuthenticationProvider.OnAuthenticationSucceded -= OnAuthenticationSucceded;
this.AuthenticationProvider.OnAuthenticationFailed -= OnAuthenticationFailed;
SetState(ConnectionStates.Closed, reason);
}
private void StartNegotiation()
{
HTTPManager.Logger.Verbose("HubConnection", "StartNegotiation");
if (this.State == ConnectionStates.CloseInitiated)
{
SetState(ConnectionStates.Closed);
return;
}
#if !BESTHTTP_DISABLE_WEBSOCKET
if (this.Options.SkipNegotiation && this.Options.PreferedTransport == TransportTypes.WebSocket)
{
HTTPManager.Logger.Verbose("HubConnection", "Skipping negotiation");
ConnectImpl(this.Options.PreferedTransport);
return;
}
#endif
SetState(ConnectionStates.Negotiating);
// https://github.com/aspnet/SignalR/blob/dev/specs/TransportProtocols.md#post-endpoint-basenegotiate-request
// Send out a negotiation request. While we could skip it and connect right with the websocket transport
// it might return with additional information that could be useful.
UriBuilder builder = new UriBuilder(this.Uri);
if (builder.Path.EndsWith("/"))
builder.Path += "negotiate";
else
builder.Path += "/negotiate";
string query = builder.Query;
if (string.IsNullOrEmpty(query))
query = "negotiateVersion=1";
else
query = query.Remove(0, 1) + "&negotiateVersion=1";
builder.Query = query;
var request = new HTTPRequest(builder.Uri, HTTPMethods.Post, OnNegotiationRequestFinished);
if (this.AuthenticationProvider != null)
this.AuthenticationProvider.PrepareRequest(request);
request.Send();
}
private void ConnectImpl(TransportTypes transport)
{
HTTPManager.Logger.Verbose("HubConnection", "ConnectImpl - " + transport);
switch (transport)
{
#if !BESTHTTP_DISABLE_WEBSOCKET
case TransportTypes.WebSocket:
if (this.NegotiationResult != null && !IsTransportSupported("WebSockets"))
{
SetState(ConnectionStates.Closed, "Couldn't use prefered transport, as the 'WebSockets' transport isn't supported by the server!");
return;
}
this.Transport = new Transports.WebSocketTransport(this);
this.Transport.OnStateChanged += Transport_OnStateChanged;
break;
#endif
case TransportTypes.LongPolling:
if (this.NegotiationResult != null && !IsTransportSupported("LongPolling"))
{
SetState(ConnectionStates.Closed, "Couldn't use prefered transport, as the 'LongPolling' transport isn't supported by the server!");
return;
}
this.Transport = new Transports.LongPollingTransport(this);
this.Transport.OnStateChanged += Transport_OnStateChanged;
break;
default:
SetState(ConnectionStates.Closed, "Unsupportted transport: " + transport);
break;
}
try
{
if (this.OnTransportEvent != null)
this.OnTransportEvent(this, this.Transport, TransportEvents.SelectedToConnect);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("HubConnection", "ConnectImpl - OnTransportEvent exception in user code!", ex);
}
this.Transport.StartConnect();
}
private bool IsTransportSupported(string transportName)
{
// https://github.com/aspnet/SignalR/blob/release/2.2/specs/TransportProtocols.md#post-endpoint-basenegotiate-request
// If the negotiation response contains only the url and accessToken, no 'availableTransports' list is sent
if (this.NegotiationResult.SupportedTransports == null)
return true;
for (int i = 0; i < this.NegotiationResult.SupportedTransports.Count; ++i)
if (this.NegotiationResult.SupportedTransports[i].Name.Equals(transportName, StringComparison.OrdinalIgnoreCase))
return true;
return false;
}
private void OnNegotiationRequestFinished(HTTPRequest req, HTTPResponse resp)
{
if (this.State == ConnectionStates.CloseInitiated)
{
SetState(ConnectionStates.Closed);
return;
}
string errorReason = null;
switch (req.State)
{
// The request finished without any problem.
case HTTPRequestStates.Finished:
if (resp.IsSuccess)
{
HTTPManager.Logger.Information("HubConnection", "Negotiation Request Finished Successfully! Response: " + resp.DataAsText);
// Parse negotiation
this.NegotiationResult = NegotiationResult.Parse(resp, out errorReason, this);
// Room for improvement: check validity of the negotiation result:
// If url and accessToken is present, the other two must be null.
// https://github.com/aspnet/SignalR/blob/dev/specs/TransportProtocols.md#post-endpoint-basenegotiate-request
if (string.IsNullOrEmpty(errorReason))
{
if (this.NegotiationResult.Url != null)
{
this.SetState(ConnectionStates.Redirected);
if (++this.RedirectCount >= this.Options.MaxRedirects)
errorReason = string.Format("MaxRedirects ({0:N0}) reached!", this.Options.MaxRedirects);
else
{
var oldUri = this.Uri;
this.Uri = this.NegotiationResult.Url;
if (this.OnRedirected != null)
{
try
{
this.OnRedirected(this, oldUri, Uri);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("HubConnection", "OnNegotiationRequestFinished - OnRedirected", ex);
}
}
StartConnect();
}
}
else
ConnectImpl(this.Options.PreferedTransport);
}
}
else // Internal server error?
errorReason = string.Format("Negotiation Request Finished Successfully, but the server sent an error. Status Code: {0}-{1} Message: {2}",
resp.StatusCode,
resp.Message,
resp.DataAsText);
break;
// The request finished with an unexpected error. The request's Exception property may contain more info about the error.
case HTTPRequestStates.Error:
errorReason = "Negotiation Request Finished with Error! " + (req.Exception != null ? (req.Exception.Message + "\n" + req.Exception.StackTrace) : "No Exception");
break;
// The request aborted, initiated by the user.
case HTTPRequestStates.Aborted:
errorReason = "Negotiation Request Aborted!";
break;
// Connecting to the server is timed out.
case HTTPRequestStates.ConnectionTimedOut:
errorReason = "Negotiation Request - Connection Timed Out!";
break;
// The request didn't finished in the given time.
case HTTPRequestStates.TimedOut:
errorReason = "Negotiation Request - Processing the request Timed Out!";
break;
}
if (errorReason != null)
{
this.NegotiationResult = new NegotiationResult();
this.NegotiationResult.NegotiationResponse = resp;
SetState(ConnectionStates.Closed, errorReason);
}
}
public void StartClose()
{
HTTPManager.Logger.Verbose("HubConnection", "StartClose");
if (this.State == ConnectionStates.Reconnecting)
SetState(ConnectionStates.Closed);
else
{
SetState(ConnectionStates.CloseInitiated);
if (this.Transport != null)
{
//this.SendMessage(new Message { type = MessageTypes.Close });
this.Transport.StartClose();
}
}
}
#if CSHARP_7_OR_LATER
TaskCompletionSource closeAsyncTaskCompletionSource;
public Task CloseAsync()
{
if (this.closeAsyncTaskCompletionSource != null)
throw new Exception("CloseAsync already called!");
this.closeAsyncTaskCompletionSource = new TaskCompletionSource();
this.OnClosed += OnClosedAsyncCallback;
this.OnError += OnClosedAsyncErrorCallback;
this.StartClose();
return this.closeAsyncTaskCompletionSource.Task;
}
void OnClosedAsyncCallback(HubConnection hub)
{
this.OnClosed -= OnClosedAsyncCallback;
this.OnError -= OnClosedAsyncErrorCallback;
this.closeAsyncTaskCompletionSource.TrySetResult(this);
this.closeAsyncTaskCompletionSource = null;
}
void OnClosedAsyncErrorCallback(HubConnection hub, string error)
{
this.OnClosed -= OnClosedAsyncCallback;
this.OnError -= OnClosedAsyncErrorCallback;
this.closeAsyncTaskCompletionSource.TrySetException(new Exception(error));
this.closeAsyncTaskCompletionSource = null;
}
#endif
public IFuture Invoke(string target, params object[] args)
{
Future future = new Future();
InvokeImp(target,
args,
(message) =>
{
bool isSuccess = string.IsNullOrEmpty(message.error);
if (isSuccess)
future.Assign((TResult)this.Protocol.ConvertTo(typeof(TResult), message.result));
else
future.Fail(new Exception(message.error));
},
typeof(TResult));
return future;
}
#if CSHARP_7_OR_LATER
public Task InvokeAsync(string target, params object[] args)
{
TaskCompletionSource tcs = new TaskCompletionSource();
InvokeImp(target,
args,
(message) =>
{
bool isSuccess = string.IsNullOrEmpty(message.error);
if (isSuccess)
tcs.TrySetResult((TResult)this.Protocol.ConvertTo(typeof(TResult), message.result));
else
tcs.TrySetException(new Exception(message.error));
},
typeof(TResult));
return tcs.Task;
}
#endif
public IFuture