├── PusherClient.public.snk ├── PusherClient ├── Properties │ ├── icon-128.png │ ├── AssemblyInfo.cs │ └── AssemblyInfo.Signed.cs ├── TextEventEmitter.cs ├── UserEventEmitter.cs ├── DynamicEventEmitter.cs ├── PusherEventEmitter.cs ├── IWatchlistFacade.cs ├── IEventEmitter.cs ├── IChannelDataDecrypter.cs ├── IPresenceChannelManagement.cs ├── IAuthorizer.cs ├── IUserFacade.cs ├── PusherSigninEvent.cs ├── IEventBinder.cs ├── PusherChannelSubscribeEvent.cs ├── ErrorConstants.cs ├── IEventBinderGeneric.cs ├── PusherChannelUnsubscribeEvent.cs ├── IAuthorizerAsync.cs ├── ITriggerChannels.cs ├── PusherChannelSubscriptionData.cs ├── IConnection.cs ├── PrivateChannel.cs ├── PresenceChannel.cs ├── EncryptedChannelData.cs ├── PusherChannelEvent.cs ├── PusherSystemEvent.cs ├── IUserAuthenticator.cs ├── UserEvent.cs ├── PusherSigninEventData.cs ├── ISerializeObjectsToJson.cs ├── IChannelAuthorizer.cs ├── WatchlistEvent.cs ├── Guard.cs ├── Exception │ ├── TriggerEventException.cs │ ├── ChannelDecryptionException.cs │ ├── OperationTimeoutException.cs │ ├── ConnectedEventHandlerException.cs │ ├── EventHandlerException.cs │ ├── DisconnectedEventHandlerException.cs │ ├── OperationException.cs │ ├── IChannelException.cs │ ├── ChannelUnauthorizedException.cs │ ├── WebsocketException.cs │ ├── ConnectionStateChangedEventHandlerException.cs │ ├── SubscribedEventHandlerException.cs │ ├── MemberAddedEventHandlerException.cs │ ├── MemberRemovedEventHandlerException.cs │ ├── PusherException.cs │ ├── EventEmitterActionException.cs │ ├── UserAuthenticationFailureException.cs │ ├── ChannelAuthorizationFailureException.cs │ └── ChannelException.cs ├── PusherAuthorizedChannelSubscriptionData.cs ├── ChannelTypes.cs ├── ITraceLogger.cs ├── IUserAuthenticatorAsync.cs ├── HttpAuthorizer.cs ├── DefaultSerializer.cs ├── IChannelAuthorizerAsync.cs ├── IPresenceChannel.cs ├── IPusher.cs ├── ConnectionState.cs ├── TraceLogger.cs ├── Constants.cs ├── ChannelDataDecrypter.cs ├── PusherClient.csproj ├── PusherEvent.cs ├── WatchlistFacade.cs ├── EventHandler.cs ├── PusherOptions.cs ├── GenericPresenceChannel.cs ├── HttpUserAuthenticator.cs ├── HttpChannelAuthorizer.cs ├── ErrorCodes.cs ├── Channel.cs ├── EventEmitter.cs └── EventEmitterGeneric.cs ├── .devcontainer └── devcontainer.json ├── AppConfig.sample.json ├── AuthHost ├── App.config ├── Program.cs ├── AuthHost.csproj └── AuthModule.cs ├── PusherClient.Tests.Utilities ├── FakeUserInfo.cs ├── IApplicationConfigLoader.cs ├── UserNameFactory.cs ├── ChannelNameFactory.cs ├── PusherClient.Tests.Utilities.csproj ├── FakeUnauthoriser.cs ├── IApplicationConfig.cs ├── ILatencyInducer.cs ├── ApplicationConfig.cs ├── EnvironmentVariableConfigLoader.cs ├── LatencyInducer.cs ├── Config.cs ├── JsonFileConfigLoader.cs ├── PusherFactory.cs └── FakeAuthoriser.cs ├── pull_request_template.md ├── PusherClient.Tests ├── AcceptanceTests │ ├── EventTestData.cs │ ├── SubscriptionTest.PrivateChannels.cs │ ├── SubscriptionTest.PresenceChannels.cs │ └── SubscriptionTest.PublicChannels.cs ├── PusherClient.Tests.csproj └── UnitTests │ ├── ChannelDataDecrypterTest.cs │ ├── PusherEventTest.cs │ └── PusherTest.cs ├── package.cmd ├── ExampleApplication ├── ChatMember.cs ├── ChatMessage.cs ├── App.config └── ExampleApplication.csproj ├── StrongName ├── GeneratePusherKey.ps1 ├── WritePusherKey.ps1 └── GenerateStrongNameKey.cmd ├── CPU.count.2.runsettings ├── .gitignore ├── Root.Build.props ├── .github ├── workflows │ ├── publish.yml │ ├── gh-release.yml │ ├── codeql.yml │ ├── build.yml │ └── release.yml └── stale.yml ├── LICENSE.txt ├── .gitattributes ├── pusher-dotnet-client.sln └── CHANGELOG.md /PusherClient.public.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pusher/pusher-websocket-dotnet/HEAD/PusherClient.public.snk -------------------------------------------------------------------------------- /PusherClient/Properties/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pusher/pusher-websocket-dotnet/HEAD/PusherClient/Properties/icon-128.png -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/vscode/devcontainers/dotnet:5.0", 3 | "features": { 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /PusherClient/TextEventEmitter.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | public class TextEventEmitter : EventEmitter 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PusherClient/UserEventEmitter.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | public class UserEventEmitter : EventEmitter 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PusherClient/DynamicEventEmitter.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | public class DynamicEventEmitter : EventEmitter 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PusherClient/PusherEventEmitter.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | public class PusherEventEmitter : EventEmitter 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AppConfig.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppId": "", 3 | "AppKey": "", 4 | "AppSecret": "", 5 | "Cluster": "mt1", 6 | "Encrypted": true, 7 | "EnableAuthorizationLatency": true 8 | } -------------------------------------------------------------------------------- /AuthHost/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /PusherClient.Tests.Utilities/FakeUserInfo.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient.Tests.Utilities 2 | { 3 | public class FakeUserInfo 4 | { 5 | public string name { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /PusherClient/IWatchlistFacade.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace PusherClient 4 | { 5 | public interface IWatchlistFacade : IEventBinder 6 | { 7 | } 8 | } -------------------------------------------------------------------------------- /PusherClient/IEventEmitter.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | public interface IEventEmitter: IEventBinder 4 | { 5 | void EmitEvent(string eventName, TData data); 6 | } 7 | } -------------------------------------------------------------------------------- /PusherClient/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("PusherClient.Tests")] 4 | [assembly: InternalsVisibleTo("PusherClient.Tests.Utilities")] -------------------------------------------------------------------------------- /PusherClient.Tests.Utilities/IApplicationConfigLoader.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient.Tests.Utilities 2 | { 3 | public interface IApplicationConfigLoader 4 | { 5 | IApplicationConfig Load(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /PusherClient/IChannelDataDecrypter.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | internal interface IChannelDataDecrypter 4 | { 5 | string DecryptData(byte[] decryptionKey, EncryptedChannelData encryptedData); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /PusherClient/IPresenceChannelManagement.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | internal interface IPresenceChannelManagement 4 | { 5 | void AddMember(string data); 6 | 7 | void RemoveMember(string data); 8 | } 9 | } -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Add a short description of the change. If this is related to an issue, please add a reference to the issue. 4 | 5 | ## CHANGELOG 6 | 7 | * [CHANGED] Describe your change here. Look at CHANGELOG.md to see the format. 8 | -------------------------------------------------------------------------------- /PusherClient/IAuthorizer.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | /// 4 | /// DEPRECATED: Use IChannelAuthorizer instead 5 | /// Contract for the authorization of a channel 6 | /// 7 | public interface IAuthorizer : IChannelAuthorizer 8 | { 9 | } 10 | } -------------------------------------------------------------------------------- /PusherClient/IUserFacade.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace PusherClient 4 | { 5 | public interface IUserFacade : IEventBinder 6 | { 7 | void Signin(); 8 | Task SigninDoneAsync(); 9 | IWatchlistFacade Watchlist { get; } 10 | 11 | } 12 | } -------------------------------------------------------------------------------- /PusherClient/PusherSigninEvent.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | internal class PusherSigninEvent : PusherSystemEvent 4 | { 5 | public PusherSigninEvent(PusherSigninEventData data) 6 | : base(Constants.PUSHER_SIGNIN, data) 7 | { 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /PusherClient.Tests.Utilities/UserNameFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient.Tests.Utilities 4 | { 5 | public static class UserNameFactory 6 | { 7 | public static string CreateUniqueUserName() 8 | { 9 | return $"testUser{Guid.NewGuid():N}"; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PusherClient.Tests/AcceptanceTests/EventTestData.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient.Tests.AcceptanceTests 2 | { 3 | public class EventTestData 4 | { 5 | public string TextField { get; set; } 6 | 7 | public string NothingField { get; set; } 8 | 9 | public int IntegerField { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /PusherClient/IEventBinder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient 4 | { 5 | public interface IEventBinder 6 | { 7 | Action ErrorHandler { get; set; } 8 | 9 | bool HasListeners { get; } 10 | 11 | void Unbind(string eventName); 12 | 13 | void UnbindAll(); 14 | } 15 | } -------------------------------------------------------------------------------- /PusherClient/PusherChannelSubscribeEvent.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | internal class PusherChannelSubscribeEvent : PusherSystemEvent 4 | { 5 | public PusherChannelSubscribeEvent(PusherChannelSubscriptionData data) 6 | : base(Constants.CHANNEL_SUBSCRIBE, data) 7 | { 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.cmd: -------------------------------------------------------------------------------- 1 | "%ProgramFiles(x86)%\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\amd64\msbuild.exe" pusher-dotnet-client.sln /t:Clean,Rebuild /p:Configuration=Release /fileLogger 2 | 3 | "%ProgramFiles(x86)%\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\amd64\msbuild.exe" PusherClient/PusherClient.csproj /t:pack /p:configuration=release -------------------------------------------------------------------------------- /ExampleApplication/ChatMember.cs: -------------------------------------------------------------------------------- 1 | namespace ExampleApplication 2 | { 3 | internal class ChatMember 4 | { 5 | public ChatMember() 6 | { 7 | } 8 | 9 | public ChatMember(string name) 10 | { 11 | this.Name = name; 12 | } 13 | 14 | public string Name { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /PusherClient/ErrorConstants.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | class ErrorConstants 4 | { 5 | public const string ApplicationKeyNotSet = "The application key cannot be null or whitespace"; 6 | public const string ConnectionAlreadyConnected = "Attempt to connect when another connection has already started. New attempt has been ignored."; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /PusherClient/IEventBinderGeneric.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient 4 | { 5 | public interface IEventBinder : IEventBinder 6 | { 7 | void Bind(string eventName, Action listener); 8 | 9 | void Bind(Action listener); 10 | 11 | void Unbind(string eventName, Action listener); 12 | } 13 | } -------------------------------------------------------------------------------- /PusherClient/PusherChannelUnsubscribeEvent.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | internal class PusherChannelUnsubscribeEvent : PusherSystemEvent 4 | { 5 | public PusherChannelUnsubscribeEvent(string channelName) 6 | : base(Constants.CHANNEL_UNSUBSCRIBE, new PusherChannelSubscriptionData(channelName)) 7 | { 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /PusherClient/IAuthorizerAsync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace PusherClient 5 | { 6 | /// 7 | /// DEPRECATED: Use IChannelAuthorizerAsync instead 8 | /// Contract for the async authorization of a channel 9 | /// 10 | public interface IAuthorizerAsync : IChannelAuthorizerAsync 11 | { 12 | } 13 | } -------------------------------------------------------------------------------- /PusherClient/ITriggerChannels.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace PusherClient 4 | { 5 | internal interface ITriggerChannels 6 | { 7 | Task TriggerAsync(string channelName, string eventName, object obj); 8 | 9 | Task ChannelUnsubscribeAsync(string channelName); 10 | 11 | void RaiseChannelError(PusherException error); 12 | } 13 | } -------------------------------------------------------------------------------- /PusherClient/PusherChannelSubscriptionData.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace PusherClient 4 | { 5 | internal class PusherChannelSubscriptionData 6 | { 7 | public PusherChannelSubscriptionData(string channelName) 8 | { 9 | this.Channel = channelName; 10 | } 11 | 12 | [JsonProperty(PropertyName = "channel")] 13 | public string Channel { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /PusherClient/IConnection.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace PusherClient 4 | { 5 | internal interface IConnection 6 | { 7 | string SocketId { get; } 8 | 9 | ConnectionState State { get; } 10 | 11 | bool IsConnected { get; } 12 | 13 | Task ConnectAsync(); 14 | 15 | Task DisconnectAsync(); 16 | 17 | Task SendAsync(string message); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /PusherClient/PrivateChannel.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | /// 4 | /// Represents a Pusher Private Channel 5 | /// 6 | public class PrivateChannel : Channel 7 | { 8 | internal PrivateChannel(string channelName, ITriggerChannels pusher) 9 | : base(channelName, pusher) 10 | { 11 | } 12 | 13 | internal byte[] SharedSecret { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /PusherClient/PresenceChannel.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | /// 4 | /// This class exists for backwards compatibility with code that isn't using GenericPresenceChannel. 5 | /// 6 | public class PresenceChannel : GenericPresenceChannel 7 | { 8 | internal PresenceChannel(string channelName, ITriggerChannels pusher) 9 | : base(channelName, pusher) 10 | { 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /ExampleApplication/ChatMessage.cs: -------------------------------------------------------------------------------- 1 | namespace ExampleApplication 2 | { 3 | internal class ChatMessage : ChatMember 4 | { 5 | public ChatMessage() 6 | : base() 7 | { 8 | } 9 | 10 | public ChatMessage(string message, string name) 11 | : base(name) 12 | { 13 | this.Message = message; 14 | } 15 | 16 | public string Message { get; set; } 17 | 18 | } 19 | } -------------------------------------------------------------------------------- /PusherClient/EncryptedChannelData.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace PusherClient 4 | { 5 | internal class EncryptedChannelData 6 | { 7 | public static EncryptedChannelData CreateFromJson(string jsonData) 8 | { 9 | return JsonConvert.DeserializeObject(jsonData); 10 | } 11 | 12 | public string nonce { get; set; } 13 | 14 | public string ciphertext { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /PusherClient/PusherChannelEvent.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace PusherClient 4 | { 5 | internal class PusherChannelEvent : PusherSystemEvent 6 | { 7 | public PusherChannelEvent(string eventName, object data, string channelName) 8 | : base(eventName, data) 9 | { 10 | this.Channel = channelName; 11 | } 12 | 13 | [JsonProperty(PropertyName = "channel")] 14 | public string Channel { get; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ExampleApplication/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /PusherClient/PusherSystemEvent.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace PusherClient 4 | { 5 | internal class PusherSystemEvent 6 | { 7 | public PusherSystemEvent(string eventName, object data) 8 | { 9 | this.Event = eventName; 10 | this.Data = data; 11 | } 12 | 13 | [JsonProperty(PropertyName = "event")] 14 | public string Event { get; } 15 | 16 | [JsonProperty(PropertyName = "data")] 17 | public object Data { get; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /PusherClient/IUserAuthenticator.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | /// 4 | /// Contract for the authentication of a user 5 | /// 6 | public interface IUserAuthenticator 7 | { 8 | /// 9 | /// Perform the authentication of the user 10 | /// 11 | /// The socket ID to use 12 | /// The response received from the authentication endpoint. 13 | string Authenticate(string socketId); 14 | } 15 | } -------------------------------------------------------------------------------- /PusherClient/UserEvent.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PusherClient 4 | { 5 | public class UserEvent 6 | { 7 | private readonly string _rawData; 8 | 9 | public UserEvent(string rawData) 10 | { 11 | _rawData = rawData; 12 | } 13 | 14 | public string Data() 15 | { 16 | return _rawData; 17 | } 18 | 19 | 20 | public override string ToString() 21 | { 22 | return _rawData; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /PusherClient/PusherSigninEventData.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace PusherClient 4 | { 5 | internal class PusherSigninEventData 6 | { 7 | public PusherSigninEventData(string auth, string userData) 8 | { 9 | this.Auth = auth; 10 | this.UserData = userData; 11 | } 12 | 13 | [JsonProperty(PropertyName = "auth")] 14 | public string Auth { get; } 15 | 16 | [JsonProperty(PropertyName = "user_data")] 17 | public string UserData { get; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /PusherClient/ISerializeObjectsToJson.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | /// 4 | /// Inteface for a Json serializer. 5 | /// 6 | public interface ISerializeObjectsToJson 7 | { 8 | /// 9 | /// Serializes an object. 10 | /// 11 | /// The object to be serialized into a Json string. 12 | /// The passed in object as a Json string. 13 | string Serialize(object objectToSerialize); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /StrongName/GeneratePusherKey.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "stop"; 2 | $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition; 3 | Push-Location $scriptDir; 4 | try { 5 | $keyName = "PusherClient"; 6 | .\GenerateStrongNameKey.cmd "$keyName" 7 | $keyBytes = [System.IO.File]::ReadAllBytes("$scriptDir\\$keyName.snk"); 8 | $keyBase64 = [System.Convert]::ToBase64String($keyBytes); 9 | Write-Output ""; 10 | Write-Output "Base 64 encoded signing key:"; 11 | Write-Output $keyBase64; 12 | } 13 | finally { 14 | Pop-Location 15 | } -------------------------------------------------------------------------------- /PusherClient/IChannelAuthorizer.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | /// 4 | /// Contract for the authorization of a channel 5 | /// 6 | public interface IChannelAuthorizer 7 | { 8 | /// 9 | /// Perform the authorization of the channel 10 | /// 11 | /// The name of the channel to authorise on 12 | /// The socket ID to use 13 | /// 14 | string Authorize(string channelName, string socketId); 15 | } 16 | } -------------------------------------------------------------------------------- /PusherClient/WatchlistEvent.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PusherClient 4 | { 5 | public class WatchlistEvent 6 | { 7 | private readonly string _rawData; 8 | public string Name { get; private set; } 9 | public List UserIDs { get; private set; } 10 | 11 | public WatchlistEvent(string name, List userIDs, string rawData) 12 | { 13 | Name = name; 14 | UserIDs = userIDs; 15 | _rawData = rawData; 16 | } 17 | 18 | public override string ToString() 19 | { 20 | return _rawData; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /PusherClient/Guard.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient 4 | { 5 | internal static class Guard 6 | { 7 | internal static void ChannelName(string channelName) 8 | { 9 | if (string.IsNullOrWhiteSpace(channelName)) 10 | { 11 | throw new ArgumentNullException(nameof(channelName)); 12 | } 13 | } 14 | 15 | internal static void EventName(string eventName) 16 | { 17 | if (string.IsNullOrWhiteSpace(eventName)) 18 | { 19 | throw new ArgumentNullException(nameof(eventName)); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /StrongName/WritePusherKey.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "stop"; 2 | $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition; 3 | Push-Location $scriptDir 4 | try { 5 | $keyText = $Env:CI_CODE_SIGN_KEY; 6 | if ($keyText) { 7 | $key = [System.Convert]::FromBase64String($keyText); 8 | $fileInfo = [System.IO.FileInfo]::new("$scriptDir\..\PusherClient.snk"); 9 | [System.IO.File]::WriteAllBytes($fileInfo.FullName, $key) | Out-Null; 10 | } 11 | else { 12 | throw "The environment variable CI_CODE_SIGN_KEY is undefined. It needs to be a base 64 encoded key."; 13 | } 14 | } 15 | finally { 16 | Pop-Location; 17 | } -------------------------------------------------------------------------------- /PusherClient/Exception/TriggerEventException.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | /// 4 | /// Raised when attempting to trigger an event. 5 | /// 6 | public class TriggerEventException : PusherException 7 | { 8 | /// 9 | /// Creates a new instance of a class. 10 | /// 11 | /// The error message. 12 | /// The Pusher error code. 13 | public TriggerEventException(string message, ErrorCodes code) 14 | : base(message, code) 15 | { 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /CPU.count.2.runsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 2 8 | 9 | 10 | .\TestResults 11 | 12 | 13 | 14 | x64 15 | 16 | -------------------------------------------------------------------------------- /PusherClient/PusherAuthorizedChannelSubscriptionData.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace PusherClient 4 | { 5 | internal class PusherAuthorizedChannelSubscriptionData : PusherChannelSubscriptionData 6 | { 7 | public PusherAuthorizedChannelSubscriptionData(string channelName, string auth, string channelData) 8 | : base(channelName) 9 | { 10 | this.Auth = auth; 11 | this.ChannelData = channelData; 12 | } 13 | 14 | [JsonProperty(PropertyName = "auth")] 15 | public string Auth { get; } 16 | 17 | [JsonProperty(PropertyName = "channel_data")] 18 | public string ChannelData { get; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PusherClient/Exception/ChannelDecryptionException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient 4 | { 5 | /// 6 | /// This exception is raised when the private encrypted channel data could not be decrypted. 7 | /// 8 | public class ChannelDecryptionException : ChannelException 9 | { 10 | /// 11 | /// Creates a new instance of a class. 12 | /// 13 | /// The exception message. 14 | public ChannelDecryptionException(string message) 15 | : base(message, ErrorCodes.ChannelDecryptionFailure, null, null) 16 | { 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /PusherClient/ChannelTypes.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | /// 4 | /// An enum represnets the different types of . 5 | /// 6 | public enum ChannelTypes 7 | { 8 | /// 9 | /// A public channel. 10 | /// 11 | Public, 12 | 13 | /// 14 | /// A private channel. 15 | /// 16 | Private, 17 | 18 | /// 19 | /// A presence channel. 20 | /// 21 | Presence, 22 | 23 | /// 24 | /// A private encrypted channel. 25 | /// 26 | PrivateEncrypted, 27 | } 28 | } -------------------------------------------------------------------------------- /AuthHost/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Configuration; 3 | using Nancy.Hosting.Self; 4 | 5 | namespace AuthHost 6 | { 7 | class Program 8 | { 9 | static void Main() 10 | { 11 | var hostUrl = "http://localhost:" + ConfigurationManager.AppSettings["Port"]; 12 | HostConfiguration hostConfigs = new HostConfiguration(); 13 | hostConfigs.UrlReservations.CreateAutomatically = true; 14 | var nancyHost = new NancyHost(hostConfigs, new Uri( hostUrl ) ); 15 | nancyHost.Start(); 16 | 17 | Console.WriteLine("Nancy host listening on " + hostUrl); 18 | 19 | Console.ReadLine(); 20 | nancyHost.Stop(); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /PusherClient/ITraceLogger.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | public interface ITraceLogger 4 | { 5 | /// 6 | /// Traces an information message. 7 | /// 8 | /// The message to trace. 9 | void TraceInformation(string message); 10 | 11 | /// 12 | /// Traces a warning message. 13 | /// 14 | /// The message to trace. 15 | void TraceWarning(string message); 16 | 17 | /// 18 | /// Traces an error message. 19 | /// 20 | /// The message to trace. 21 | void TraceError(string message); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio Code 2 | .vscode 3 | 4 | # Rider 5 | .idea 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | [Oo]ut/ 25 | msbuild.log 26 | msbuild.err 27 | msbuild.wrn 28 | 29 | # Visual Studio 30 | .vs/ 31 | TestResults/ 32 | *.testsettings 33 | *.vsmdi 34 | 35 | # Secrets file 36 | AppConfig.test.json 37 | 38 | # Signing key 39 | PusherClient.snk 40 | 41 | # Custom 42 | _ReSharper.*/ 43 | packages/ 44 | nupkg/ 45 | _UpgradeReport_Files 46 | UpgradeLog.XML 47 | Backup 48 | Download 49 | *.swp 50 | *.*~ 51 | project.lock.json 52 | .DS_Store 53 | *.pyc -------------------------------------------------------------------------------- /PusherClient/IUserAuthenticatorAsync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace PusherClient 5 | { 6 | /// 7 | /// Contract for the authentication of a user 8 | /// 9 | public interface IUserAuthenticatorAsync 10 | { 11 | /// 12 | /// Gets or sets the timeout period for the authenticator. This property is optional. 13 | /// 14 | TimeSpan? Timeout { get; set; } 15 | 16 | /// 17 | /// Perform the authentication of the user 18 | /// 19 | /// The socket ID to use 20 | /// The response received from the authentication endpoint. 21 | Task AuthenticateAsync( string socketId); 22 | } 23 | } -------------------------------------------------------------------------------- /PusherClient/HttpAuthorizer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Net.Http.Headers; 6 | using System.Threading.Tasks; 7 | 8 | namespace PusherClient 9 | { 10 | /// 11 | /// DEPRECATED: Use ChannelAuthorizer = new HttpChannelAuthorizer("http://example.com/auth") instead 12 | /// An implementation of the using Http 13 | /// 14 | public class HttpAuthorizer: HttpChannelAuthorizer, IAuthorizer, IAuthorizerAsync 15 | { 16 | /// 17 | /// ctor 18 | /// 19 | /// The End point to contact 20 | public HttpAuthorizer(string authEndpoint) : base(authEndpoint) 21 | { 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /PusherClient/Properties/AssemblyInfo.Signed.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("PusherClient.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100dde7ca64f1697af97d0e05c2e9fe2d6fcb614d4fbc3572e8a138397f379a94e41cca469ca8af531708bb7ea31da04a5db77d491a7a48e9ecdd2dc746888e91b7ec923f719b45f118c5506333979d6866bca1a6857ff51246453d8786fa322f83c155e2adfe5be34d2e4a3fb8afd7b6d7eee4704f4a589efee89584a42cb358c1")] 4 | [assembly: InternalsVisibleTo("PusherClient.Tests.Utilities, PublicKey=0024000004800000940000000602000000240000525341310004000001000100dde7ca64f1697af97d0e05c2e9fe2d6fcb614d4fbc3572e8a138397f379a94e41cca469ca8af531708bb7ea31da04a5db77d491a7a48e9ecdd2dc746888e91b7ec923f719b45f118c5506333979d6866bca1a6857ff51246453d8786fa322f83c155e2adfe5be34d2e4a3fb8afd7b6d7eee4704f4a589efee89584a42cb358c1")] -------------------------------------------------------------------------------- /PusherClient/DefaultSerializer.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace PusherClient 4 | { 5 | /// 6 | /// Default implmentation for serializing an object 7 | /// 8 | public class DefaultSerializer : ISerializeObjectsToJson 9 | { 10 | private static readonly JsonSerializerSettings _settings = new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore }; 11 | 12 | /// 13 | /// Gets the static default serializer. 14 | /// 15 | public static ISerializeObjectsToJson Default { get; } = new DefaultSerializer(); 16 | 17 | /// 18 | public string Serialize(object objectToSerialize) 19 | { 20 | return JsonConvert.SerializeObject(objectToSerialize, _settings); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /PusherClient/Exception/OperationTimeoutException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient 4 | { 5 | /// 6 | /// Raised when a client timeout occurs waiting for an asynchrounous operation to complete. 7 | /// 8 | public class OperationTimeoutException : PusherException 9 | { 10 | /// 11 | /// Creates a new instance of a class. 12 | /// 13 | /// The timeout period. 14 | /// Identifies the client operation that timed out. 15 | public OperationTimeoutException(TimeSpan timeoutPeriod, string operation) 16 | : base($"Waiting for '{operation}' has timed out after {timeoutPeriod.TotalSeconds:N} second(s).", ErrorCodes.ClientTimeout) 17 | { 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PusherClient/Exception/ConnectedEventHandlerException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient 4 | { 5 | /// 6 | /// An instance of this class gets passed to the Pusher Error delegate when the Connected delegate raises an unexpected exception. 7 | /// 8 | public class ConnectedEventHandlerException : EventHandlerException 9 | { 10 | /// 11 | /// Creates a new instance of a class. 12 | /// 13 | /// The exception that caused the current exception. 14 | public ConnectedEventHandlerException(Exception innerException) 15 | : base($"Error invoking the Connected delegate:{Environment.NewLine}{innerException.Message}", ErrorCodes.ConnectedEventHandlerError, innerException) 16 | { 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /PusherClient/Exception/EventHandlerException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient 4 | { 5 | /// 6 | /// An instance of this class gets emitted to the Pusher Error delegate whenever an event handler raises an unexpected exception. 7 | /// 8 | public class EventHandlerException : PusherException 9 | { 10 | /// 11 | /// Creates a new instance of a class. 12 | /// 13 | /// The exception message. 14 | /// The Pusher error code. 15 | /// The exception that caused the current exception. 16 | public EventHandlerException(string message, ErrorCodes code, Exception innerException) 17 | : base(message, code, innerException) 18 | { 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PusherClient/IChannelAuthorizerAsync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace PusherClient 5 | { 6 | /// 7 | /// Contract for the async authorization of a channel 8 | /// 9 | public interface IChannelAuthorizerAsync 10 | { 11 | /// 12 | /// Gets or sets the timeout period for the authorizer. This property is optional. 13 | /// 14 | TimeSpan? Timeout { get; set; } 15 | 16 | /// 17 | /// Perform the authorization of the channel 18 | /// 19 | /// The name of the channel to authorise on 20 | /// The socket ID to use 21 | /// The response received from the authorization endpoint. 22 | Task AuthorizeAsync(string channelName, string socketId); 23 | } 24 | } -------------------------------------------------------------------------------- /PusherClient/Exception/DisconnectedEventHandlerException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient 4 | { 5 | /// 6 | /// An instance of this class gets passed to the Pusher Error delegate when the Disconnected delegate raises an unexpected exception. 7 | /// 8 | public class DisconnectedEventHandlerException : EventHandlerException 9 | { 10 | /// 11 | /// Creates a new instance of a class. 12 | /// 13 | /// The exception that is the cause of the current exception. 14 | public DisconnectedEventHandlerException(Exception innerException) 15 | : base($"Error invoking the Disconnected delegate:{Environment.NewLine}{innerException.Message}", ErrorCodes.DisconnectedEventHandlerError, innerException) 16 | { 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Root.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PusherClient 5 | Pusher .NET Client Library 6 | Pusher 7 | Pusher 8 | false 9 | pusher realtime websocket 10 | https://github.com/pusher/pusher-websocket-dotnet 11 | Copyright © 2021 12 | The .NET library for interacting with the Pusher WebSocket API. Register at http://pusher.com 13 | MIT 14 | icon-128.png 15 | 2.3.1 16 | 2.3.1.0 17 | 2.3.1.0 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /ExampleApplication/ExampleApplication.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net45 4 | true 5 | false 6 | Exe 7 | Pusher 8 | Pusher.com 9 | PusherClient 10 | Copyright © Pusher 2021 11 | An Example App for user with Pusher Client 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /PusherClient/Exception/OperationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient 4 | { 5 | /// 6 | /// Raised when a client operation generates an unexpected error. 7 | /// 8 | public class OperationException : PusherException 9 | { 10 | /// 11 | /// Creates a new instance of an class. 12 | /// 13 | /// The Pusher error code. 14 | /// Identifies the client operation that errored. 15 | /// The exception that caused the current exception. 16 | public OperationException(ErrorCodes code, string operation, Exception innerException) 17 | : base($"An unexpected error was detected when performing the operation '{operation}':{Environment.NewLine}{innerException.Message}", code, innerException) 18 | { 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PusherClient/Exception/IChannelException.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | public interface IChannelException 4 | { 5 | /// 6 | /// Gets or sets the name of the channel for which the exception occured. 7 | /// 8 | string ChannelName { get; set; } 9 | 10 | /// 11 | /// Gets or sets the event name for which the exception occured. Note that this property is not always available and can be null. 12 | /// 13 | string EventName { get; set; } 14 | 15 | /// 16 | /// Gets or sets the channel socket ID. 17 | /// 18 | string SocketID { get; set; } 19 | 20 | /// 21 | /// Gets or sets the that errored if available. 22 | /// Note that this value can be null because the Channel object is not always available. 23 | /// 24 | Channel Channel { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /PusherClient/IPresenceChannel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PusherClient 4 | { 5 | /// 6 | /// Interface for a presence channel. 7 | /// 8 | /// Channel member detail. 9 | public interface IPresenceChannel 10 | { 11 | /// 12 | /// Gets a member using the member's user ID. 13 | /// 14 | /// The member's user ID. 15 | /// Retruns the member if found; otherwise returns null. 16 | T GetMember(string userId); 17 | 18 | /// 19 | /// Gets the current list of members as a where the TKey is the user ID and TValue is the member detail. 20 | /// 21 | /// Returns a containing the current members. 22 | Dictionary GetMembers(); 23 | } 24 | } -------------------------------------------------------------------------------- /PusherClient/IPusher.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace PusherClient 4 | { 5 | internal interface IPusher 6 | { 7 | PusherOptions PusherOptions { get; set; } 8 | 9 | void ChangeConnectionState(ConnectionState state); 10 | void ErrorOccured(PusherException pusherException); 11 | void AddMember(string channelName, string member); 12 | void RemoveMember(string channelName, string member); 13 | void SubscriptionSuceeded(string channelName, string data); 14 | void SubscriberCount(string channelName, string data); 15 | void SubscriptionFailed(string channelName, string data); 16 | IEventBinder GetEventBinder(string eventBinderKey); 17 | IEventBinder GetChannelEventBinder(string eventBinderKey, string channelName); 18 | byte[] GetSharedSecret(string channelName); 19 | 20 | Task SubscribeAsync(string channelName, SubscriptionEventHandler subscribedEventHandler = null); 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: windows-2025 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Setup MS Build 15 | uses: microsoft/setup-msbuild@v1.0.2 16 | - name: Restore dependencies 17 | run: nuget restore pusher-dotnet-client.sln 18 | - name: Write code signing key 19 | env: 20 | CI_CODE_SIGN_KEY: ${{ secrets.CI_CODE_SIGN_KEY }} 21 | run: | 22 | ./StrongName/WritePusherKey.ps1 23 | - name: Build 24 | run: msbuild /p:SignAssembly=true /p:deterministic=true /p:msbuildArchitecture=x64 /p:configuration=Release pusher-dotnet-client.sln 25 | - name: Pack 26 | run: msbuild /t:Pack /p:SignAssembly=true /p:configuration=release PusherClient/PusherClient.csproj 27 | - name: Publish 28 | run: nuget push PusherClient\bin\release\PusherClient.*.nupkg -NonInteractive -Source https://api.nuget.org/v3/index.json -SkipDuplicate -ApiKey ${{ secrets.NUGET_API_KEY }} 29 | -------------------------------------------------------------------------------- /PusherClient.Tests.Utilities/ChannelNameFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient.Tests.Utilities 4 | { 5 | public static class ChannelNameFactory 6 | { 7 | public static string CreateUniqueChannelName(ChannelTypes channelType = ChannelTypes.Public) 8 | { 9 | string channelPrefix; 10 | switch (channelType) 11 | { 12 | case ChannelTypes.Private: 13 | channelPrefix = "private-"; 14 | break; 15 | case ChannelTypes.PrivateEncrypted: 16 | channelPrefix = "private-encrypted-"; 17 | break; 18 | case ChannelTypes.Presence: 19 | channelPrefix = "presence-"; 20 | break; 21 | default: 22 | channelPrefix = string.Empty; 23 | break; 24 | } 25 | 26 | var mockChannelName = $"{channelPrefix}-test-{Guid.NewGuid():N}"; 27 | return mockChannelName; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/gh-release.yml: -------------------------------------------------------------------------------- 1 | name: Github Release 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | create-release: 9 | name: Create Release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | - name: Setup git 15 | run: | 16 | git config user.email "pusher-ci@pusher.com" 17 | git config user.name "Pusher CI" 18 | - name: Prepare description 19 | run: | 20 | csplit -s CHANGELOG.md "/##/" {1} 21 | cat xx01 > CHANGELOG.tmp 22 | - name: Prepare tag 23 | run: | 24 | export TAG=$(head -1 CHANGELOG.tmp | cut -d' ' -f2) 25 | echo "TAG=$TAG" >> $GITHUB_ENV 26 | - name: Create Release 27 | uses: actions/create-release@v1 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | with: 31 | tag_name: ${{ env.TAG }} 32 | release_name: ${{ env.TAG }} 33 | body_path: CHANGELOG.tmp 34 | draft: false 35 | prerelease: false 36 | -------------------------------------------------------------------------------- /PusherClient/Exception/ChannelUnauthorizedException.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | /// 4 | /// This exception is raised when calling Authorize or AuthorizeAsync on and access to the channel is forbidden (403). 5 | /// 6 | public class ChannelUnauthorizedException : ChannelAuthorizationFailureException 7 | { 8 | /// 9 | /// Creates a new instance of a class. 10 | /// 11 | /// The authorization endpoint URL. 12 | /// The name of the channel for which access is forbidden. 13 | /// the socket ID used in the authorization attempt. 14 | public ChannelUnauthorizedException(string authorizationEndpoint, string channelName, string socketId) 15 | : base($"Unauthorized subscription for channel {channelName}.", ErrorCodes.ChannelUnauthorized, authorizationEndpoint, channelName, socketId) 16 | { 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Pusher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /AuthHost/AuthHost.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net45 4 | true 5 | false 6 | Exe 7 | Pusher 8 | Pusher.com 9 | PusherClient 10 | Copyright © Pusher 2021 11 | An example authentication host for use with the Pusher example app 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /PusherClient.Tests.Utilities/PusherClient.Tests.Utilities.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net45 4 | true 5 | false 6 | Pusher 7 | Pusher.com 8 | PusherClient 9 | Copyright © Pusher 2021 10 | Utilities for the Pusher Tests 11 | false 12 | 13 | 14 | 15 | ..\PusherClient.snk 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 90 5 | 6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 8 | daysUntilClose: 7 9 | 10 | # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) 11 | onlyLabels: [] 12 | 13 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 14 | exemptLabels: 15 | - pinned 16 | - security 17 | 18 | # Set to true to ignore issues with an assignee (defaults to false) 19 | exemptAssignees: true 20 | 21 | # Comment to post when marking as stale. Set to `false` to disable 22 | markComment: > 23 | This issue has been automatically marked as stale because it has not had 24 | recent activity. It will be closed if no further activity occurs. If you'd 25 | like this issue to stay open please leave a comment indicating how this issue 26 | is affecting you. Thank you. 27 | -------------------------------------------------------------------------------- /PusherClient/ConnectionState.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | /// 4 | /// An enum for describing the different states the Pusher client can be in 5 | /// 6 | public enum ConnectionState 7 | { 8 | /// 9 | /// The initial state 10 | /// 11 | Uninitialized, 12 | 13 | /// 14 | /// The state when the connection process has begun 15 | /// 16 | Connecting, 17 | 18 | /// 19 | /// The state when the connection process has completed successfully 20 | /// 21 | Connected, 22 | 23 | /// 24 | /// The state when the disconnection process has begun 25 | /// 26 | Disconnecting, 27 | 28 | /// 29 | /// The state when the disconnection process has completed, or the connection was dropped 30 | /// 31 | Disconnected, 32 | 33 | /// 34 | /// The state when a connection retry is in process 35 | /// 36 | WaitingToReconnect, 37 | } 38 | } -------------------------------------------------------------------------------- /PusherClient/Exception/WebsocketException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient 4 | { 5 | /// 6 | /// An instance of this class gets passed to the Pusher Error delegate when a error is emitted. 7 | /// 8 | public class WebsocketException : PusherException 9 | { 10 | /// 11 | /// Creates a new instance of a class. 12 | /// 13 | /// The state of the connection at the time the error. 14 | /// The exception that is the cause of the error. 15 | public WebsocketException(ConnectionState state, Exception innerException) 16 | : base($"{nameof(WebSocket4Net.WebSocket)} error emitted:{Environment.NewLine}{innerException.Message}", ErrorCodes.WebSocketError, innerException) 17 | { 18 | this.State = state; 19 | } 20 | 21 | /// 22 | /// Gets the at the time of error. 23 | /// 24 | public ConnectionState State { get; private set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /PusherClient.Tests.Utilities/FakeUnauthoriser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace PusherClient.Tests.Utilities 5 | { 6 | public class FakeUnauthoriser : IAuthorizer 7 | { 8 | public const string UnauthoriseToken = "-unauth"; 9 | 10 | public FakeUnauthoriser() 11 | { 12 | } 13 | 14 | public string Authorize(string channelName, string socketId) 15 | { 16 | double delay = LatencyInducer.InduceLatency(FakeAuthoriser.MinLatency, FakeAuthoriser.MaxLatency) / 1000.0; 17 | Trace.TraceInformation($"{this.GetType().Name} paused for {Math.Round(delay, 3)} second(s)"); 18 | if (channelName.Contains(UnauthoriseToken)) 19 | { 20 | throw new ChannelUnauthorizedException($"https://localhost/{nameof(FakeUnauthoriser)}", channelName, socketId); 21 | } 22 | else 23 | { 24 | throw new ChannelAuthorizationFailureException("Endpoint not found.", ErrorCodes.ChannelAuthorizationError, $"https://does.not.exist/{nameof(FakeUnauthoriser)}", channelName, socketId); 25 | } 26 | } 27 | 28 | private static ILatencyInducer LatencyInducer { get; } = new LatencyInducer(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /PusherClient.Tests.Utilities/IApplicationConfig.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient.Tests.Utilities 2 | { 3 | /// 4 | /// The test application configuration settings. 5 | /// 6 | public interface IApplicationConfig 7 | { 8 | /// 9 | /// Gets or sets the Pusher application id. 10 | /// 11 | string AppId { get; set; } 12 | 13 | /// 14 | /// Gets or sets the Pusher application key. 15 | /// 16 | string AppKey { get; set; } 17 | 18 | /// 19 | /// Gets or sets the Pusher application secret. 20 | /// 21 | string AppSecret { get; set; } 22 | 23 | /// 24 | /// Gets or sets the Pusher application cluster. 25 | /// 26 | string Cluster { get; set; } 27 | 28 | /// 29 | /// Gets or sets whether the connection will be encrypted. 30 | /// 31 | bool Encrypted { get; set; } 32 | 33 | /// 34 | /// Gets or sets whether an artificial latency is induced when authorizing a channel. 35 | /// 36 | bool? EnableAuthorizationLatency { get; set; } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /StrongName/GenerateStrongNameKey.cmd: -------------------------------------------------------------------------------- 1 | @if '%1' == '' GOTO EXIT_WITH_ERROR 2 | 3 | @set "WIP_FILE=C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\VsDevCmd.bat" 4 | @if exist %WIP_FILE% ( 5 | @call "%WIP_FILE%" 6 | GOTO GENERATE_SNK 7 | ) 8 | 9 | @set "WIP_FILE=%VS150COMNTOOLS%\vsvars32.bat" 10 | @if exist "%WIP_FILE%" ( 11 | @call "%WIP_FILE%" 12 | GOTO GENERATE_SNK 13 | ) 14 | 15 | @set "WIP_FILE=%VS140COMNTOOLS%\vsvars32.bat" 16 | @if exist %WIP_FILE% ( 17 | @call "%WIP_FILE%" 18 | GOTO GENERATE_SNK 19 | ) 20 | 21 | @set "WIP_FILE=%VS130COMNTOOLS%\vsvars32.bat" 22 | @if exist %WIP_FILE% ( 23 | @call "%WIP_FILE%" 24 | GOTO GENERATE_SNK 25 | ) 26 | 27 | @set "WIP_FILE=%VS120COMNTOOLS%\vsvars32.bat" 28 | @if exist %WIP_FILE% ( 29 | @call "%WIP_FILE%" 30 | GOTO GENERATE_SNK 31 | ) 32 | 33 | @set "WIP_FILE=%VS110COMNTOOLS%\vsvars32.bat" 34 | @if exist %WIP_FILE% ( 35 | @call "%WIP_FILE%" 36 | GOTO GENERATE_SNK 37 | ) 38 | 39 | @set "WIP_FILE=%VS100COMNTOOLS%\vsvars32.bat" 40 | @if exist %WIP_FILE% ( 41 | @call "%WIP_FILE%" 42 | GOTO GENERATE_SNK 43 | ) 44 | 45 | :GENERATE_SNK 46 | sn -k "%~1.snk" 47 | sn -p "%~1.snk" "%~1.public.snk" 48 | sn -tp "%~1.public.snk" 49 | exit /B 0 50 | 51 | :EXIT_WITH_ERROR 52 | @echo Please provide a strong name key file name as a parameter to this command file. 53 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ master, develop ] 6 | pull_request: 7 | branches: [ master ] 8 | schedule: 9 | - cron: '23 12 * * 1' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: windows-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ 'csharp' ] 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v3 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: security-and-quality 34 | 35 | - name: Setup MSBuild 36 | uses: microsoft/setup-msbuild@v2 37 | 38 | - name: Setup NuGet 39 | uses: nuget/setup-nuget@v1 40 | with: 41 | nuget-version: 'latest' 42 | 43 | - name: Restore dependencies 44 | run: nuget restore pusher-dotnet-client.sln 45 | 46 | - name: Build 47 | run: msbuild /p:deterministic=true /p:msbuildArchitecture=x64 /p:configuration=Release pusher-dotnet-client.sln 48 | 49 | - name: Perform CodeQL Analysis 50 | uses: github/codeql-action/analyze@v3 51 | with: 52 | category: "/language:${{matrix.language}}" -------------------------------------------------------------------------------- /PusherClient/Exception/ConnectionStateChangedEventHandlerException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient 4 | { 5 | /// 6 | /// An instance of this class gets passed to the Pusher Error delegate when the ConnectionStateChanged delegate raises an unexpected exception. 7 | /// 8 | public class ConnectionStateChangedEventHandlerException : EventHandlerException 9 | { 10 | /// 11 | /// Creates a new instance of a class. 12 | /// 13 | /// The state passed to the delegate ConnectionStateChanged at the time of the error. 14 | /// The exception that is the cause of the current exception. 15 | public ConnectionStateChangedEventHandlerException(ConnectionState state, Exception innerException) 16 | : base($"Error invoking the ConnectionStateChanged delegate:{Environment.NewLine}{innerException.Message}", ErrorCodes.ConnectionStateChangedEventHandlerError, innerException) 17 | { 18 | this.State = state; 19 | } 20 | 21 | /// 22 | /// Gets the change that caused the error. 23 | /// 24 | public ConnectionState State { get; private set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /PusherClient/Exception/SubscribedEventHandlerException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient 4 | { 5 | /// 6 | /// An instance of this class gets passed to the Pusher Error delegate when the Subscribed delegate raises an unexpected exception. 7 | /// 8 | public class SubscribedEventHandlerException : EventHandlerException 9 | { 10 | /// 11 | /// Creates a new instance of a class. 12 | /// 13 | /// The channel for which the exception occured. 14 | /// The exception that is the cause of the current exception. 15 | /// The channel's message data. 16 | public SubscribedEventHandlerException(Channel channel, Exception innerException, string data) 17 | : base($"Error invoking the Subscribed delegate:{Environment.NewLine}{innerException.Message}", ErrorCodes.SubscribedEventHandlerError, innerException) 18 | { 19 | this.Channel = channel; 20 | this.MessageData = data; 21 | } 22 | 23 | /// 24 | /// Gets the channel for which the exception occured. 25 | /// 26 | public Channel Channel { get; private set; } 27 | 28 | /// 29 | /// Gets the channel's message data. 30 | /// 31 | public string MessageData { get; private set; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /PusherClient/TraceLogger.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace PusherClient 4 | { 5 | /// 6 | /// Used to trace debug messages. 7 | /// 8 | public class TraceLogger : ITraceLogger 9 | { 10 | private static TraceSource Trace { get; } = new TraceSource(nameof(Pusher)); 11 | 12 | /// 13 | /// Traces an error message. 14 | /// 15 | /// The message to trace. 16 | public void TraceError(string message) 17 | { 18 | if (message != null) 19 | { 20 | Trace.TraceEvent(TraceEventType.Error, -1, message); 21 | } 22 | } 23 | 24 | /// 25 | /// Traces an information message. 26 | /// 27 | /// The message to trace. 28 | public void TraceInformation(string message) 29 | { 30 | if (message != null) 31 | { 32 | Trace.TraceEvent(TraceEventType.Information, 0, message); 33 | } 34 | } 35 | 36 | /// 37 | /// Traces a warning message. 38 | /// 39 | /// The message to trace. 40 | public void TraceWarning(string message) 41 | { 42 | if (message != null) 43 | { 44 | Trace.TraceEvent(TraceEventType.Warning, 1, message); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [ master, develop ] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: windows-2025 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Setup MS Build 16 | uses: microsoft/setup-msbuild@v1.0.2 17 | - name: Setup VSTest 18 | uses: darenm/Setup-VSTest@v1 19 | - name: Restore dependencies 20 | run: nuget restore pusher-dotnet-client.sln 21 | - name: Build 22 | run: msbuild /p:deterministic=true /p:msbuildArchitecture=x64 /p:configuration=Release pusher-dotnet-client.sln 23 | - name: Test 24 | env: 25 | PUSHER_APP_ID: ${{ secrets.CI_APP_ID }} 26 | PUSHER_APP_KEY: ${{ secrets.CI_APP_KEY }} 27 | PUSHER_APP_SECRET: ${{ secrets.CI_APP_SECRET }} 28 | PUSHER_APP_CLUSTER: ${{ secrets.CI_APP_CLUSTER }} 29 | run: vstest.console.exe /Parallel /Platform:x64 "./PusherClient.Tests/bin/Release/net45/PusherClient.Tests.dll" /TestAdapterPath:"./PusherClient.Tests/bin/Release/net45" 30 | - name: Write code signing key 31 | env: 32 | CI_CODE_SIGN_KEY: ${{ secrets.CI_CODE_SIGN_KEY }} 33 | run: | 34 | ./StrongName/WritePusherKey.ps1 35 | - name: Test strong name signing 36 | run: msbuild /p:SignAssembly=true /p:deterministic=true /p:msbuildArchitecture=x64 /p:configuration=Release pusher-dotnet-client.sln 37 | - name: Test pack with strong named assembly 38 | run: msbuild /t:Pack /p:SignAssembly=true /p:configuration=release PusherClient/PusherClient.csproj 39 | -------------------------------------------------------------------------------- /PusherClient.Tests.Utilities/ILatencyInducer.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace PusherClient.Tests.Utilities 4 | { 5 | /// 6 | /// Interface for a latency inducer. 7 | /// 8 | public interface ILatencyInducer 9 | { 10 | /// 11 | /// Gets or sets whether this latency inducer is enabled. 12 | /// 13 | bool Enabled { get; set; } 14 | 15 | /// 16 | /// If enabled, pauses for a random period between and . 17 | /// 18 | /// The minimum latency to induce measured in milli-seconds. 19 | /// The maximum latency to induce measured in milli-seconds. 20 | /// The number of milli-seconds that was induced. Will return 0 if disabled. 21 | int InduceLatency(int minLatency, int maxLatency); 22 | 23 | /// 24 | /// If enabled, pauses for a random period between and . 25 | /// 26 | /// The minimum latency to induce measured in milli-seconds. 27 | /// The maximum latency to induce measured in milli-seconds. 28 | /// The number of milli-seconds that was induced. Will return 0 if disabled. 29 | Task InduceLatencyAsync(int minLatency, int maxLatency); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /PusherClient/Exception/MemberAddedEventHandlerException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient 4 | { 5 | /// 6 | /// An instance of this class gets passed to the Pusher Error delegate when the MemberAdded delegate raises an unexpected exception. 7 | /// 8 | /// The member detail. 9 | public class MemberAddedEventHandlerException : EventHandlerException 10 | { 11 | /// 12 | /// Creates a new instance of a class. 13 | /// 14 | /// The key for the member that caused this exception. 15 | /// The detail of the member. 16 | /// The exception that is the cause of the current exception. 17 | public MemberAddedEventHandlerException(string memberKey, T member, Exception innerException) 18 | : base($"Error invoking the MemberAdded delegate:{Environment.NewLine}{innerException.Message}", ErrorCodes.MemberAddedEventHandlerError, innerException) 19 | { 20 | this.MemberKey = memberKey; 21 | this.Member = member; 22 | } 23 | 24 | /// 25 | /// Gets the key for the member that caused this exception. 26 | /// 27 | public string MemberKey { get; private set; } 28 | 29 | /// 30 | /// Gets the detail of the member. 31 | /// 32 | public T Member { get; private set; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /PusherClient/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | class Constants 4 | { 5 | public const string PUSHER_MESSAGE_PREFIX = "pusher"; 6 | public const string ERROR = "pusher:error"; 7 | 8 | public const string CONNECTION_ESTABLISHED = "pusher:connection_established"; 9 | 10 | public const string CHANNEL_SUBSCRIBE = "pusher:subscribe"; 11 | public const string CHANNEL_UNSUBSCRIBE = "pusher:unsubscribe"; 12 | public const string CHANNEL_SUBSCRIPTION_SUCCEEDED = "pusher_internal:subscription_succeeded"; 13 | public const string CHANNEL_SUBSCRIPTION_COUNT = "pusher_internal:subscription_count"; 14 | public const string CHANNEL_SUBSCRIPTION_ERROR = "pusher_internal:subscription_error"; 15 | public const string CHANNEL_MEMBER_ADDED = "pusher_internal:member_added"; 16 | public const string CHANNEL_MEMBER_REMOVED = "pusher_internal:member_removed"; 17 | 18 | public const string PUSHER_SIGNIN = "pusher:signin"; 19 | public const string PUSHER_SIGNIN_SUCCESS = "pusher:signin_success"; 20 | public const string PUSHER_WATCHLIST_EVENT = "pusher_internal:watchlist_events"; 21 | 22 | public const string USER_CHANNEL_PREFIX = "#server-to-user-"; 23 | 24 | public const string INSECURE_SCHEMA = "ws://"; 25 | public const string SECURE_SCHEMA = "wss://"; 26 | 27 | public const string PRIVATE_CHANNEL = "private-"; 28 | public const string PRIVATE_ENCRYPTED_CHANNEL = "private-encrypted-"; 29 | public const string PRESENCE_CHANNEL = "presence-"; 30 | } 31 | } -------------------------------------------------------------------------------- /PusherClient/Exception/MemberRemovedEventHandlerException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient 4 | { 5 | /// 6 | /// An instance of this class gets passed to the Pusher Error delegate when the MemberRemoved delegate raises an unexpected exception. 7 | /// 8 | /// The member detail. 9 | public class MemberRemovedEventHandlerException : EventHandlerException 10 | { 11 | /// 12 | /// Creates a new instance of a class. 13 | /// 14 | /// The key for the member that caused this exception. 15 | /// The detail of the member. 16 | /// The exception that is the cause of the current exception. 17 | public MemberRemovedEventHandlerException(string memberKey, T member, Exception innerException) 18 | : base($"Error invoking the MemberRemoved delegate:{Environment.NewLine}{innerException.Message}", ErrorCodes.MemberRemovedEventHandlerError, innerException) 19 | { 20 | this.MemberKey = memberKey; 21 | this.Member = member; 22 | } 23 | 24 | /// 25 | /// Gets the key for the member that caused this exception. 26 | /// 27 | public string MemberKey { get; private set; } 28 | 29 | /// 30 | /// Gets the detail of the member. 31 | /// 32 | public T Member { get; private set; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /PusherClient.Tests/PusherClient.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net45 4 | true 5 | false 6 | Pusher 7 | Pusher.com 8 | PusherClient 9 | Copyright © Pusher 2021 10 | Tests for the .NET Pusher Client 11 | false 12 | 13 | 14 | 15 | ..\PusherClient.snk 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /PusherClient.Tests.Utilities/ApplicationConfig.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient.Tests.Utilities 2 | { 3 | /// 4 | /// The test application configuration settings. 5 | /// 6 | public class ApplicationConfig : IApplicationConfig 7 | { 8 | /// 9 | /// Instantiates an instance of an class. 10 | /// 11 | public ApplicationConfig() 12 | { 13 | this.EnableAuthorizationLatency = true; 14 | } 15 | 16 | /// 17 | /// Gets or sets the Pusher application id. 18 | /// 19 | public string AppId { get; set; } 20 | 21 | /// 22 | /// Gets or sets the Pusher application key. 23 | /// 24 | public string AppKey { get; set; } 25 | 26 | /// 27 | /// Gets or sets the Pusher application secret. 28 | /// 29 | public string AppSecret { get; set; } 30 | 31 | /// 32 | /// Gets or sets the Pusher application cluster. 33 | /// 34 | public string Cluster { get; set; } 35 | 36 | /// 37 | /// Gets or sets whether the connection will be encrypted. 38 | /// 39 | public bool Encrypted { get; set; } 40 | 41 | /// 42 | /// Gets or sets whether an artificial latency is induced when authorizing a channel. 43 | /// 44 | public bool? EnableAuthorizationLatency { get; set; } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /PusherClient/Exception/PusherException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient 4 | { 5 | /// 6 | /// A Pusher Exception 7 | /// 8 | public class PusherException : Exception 9 | { 10 | /// 11 | /// Creates a new instance of a class. 12 | /// 13 | /// The exception message. 14 | /// The Pusher error code. 15 | public PusherException(string message, ErrorCodes code) 16 | : base(message) 17 | { 18 | PusherCode = code; 19 | } 20 | 21 | /// 22 | /// Creates a new instance of a class. 23 | /// 24 | /// The exception message. 25 | /// The Pusher error code. 26 | /// The exception that is the cause of the current exception. 27 | public PusherException(string message, ErrorCodes code, Exception innerException) 28 | : base(message, innerException) 29 | { 30 | PusherCode = code; 31 | } 32 | 33 | /// 34 | /// Gets the Pusher error code 35 | /// 36 | public ErrorCodes PusherCode { get; } 37 | 38 | /// 39 | /// Gets or sets whether this exception has been emitted to the Pusher.Error event handler. 40 | /// 41 | /// This property helps prevent duplicate error events from being emitted. 42 | public bool EmittedToErrorHandler { get; set; } 43 | } 44 | } -------------------------------------------------------------------------------- /PusherClient/Exception/EventEmitterActionException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient 4 | { 5 | /// 6 | /// An instance of this class gets emitted to the Pusher Error delegate whenever an action raises an unexpected exception. 7 | /// 8 | /// The event data type. 9 | public class EventEmitterActionException : PusherException 10 | { 11 | /// 12 | /// Creates a new instance of a class. 13 | /// 14 | /// The Pusher error code. 15 | /// The event name for the action that raised an error. 16 | /// The data for the event action that raised the error. 17 | /// The exception that caused the current exception. 18 | public EventEmitterActionException(ErrorCodes code, string eventName, TData data, Exception innerException) 19 | : base($"Error invoking the action for the emitted event {eventName}:{Environment.NewLine}{innerException.Message}", code, innerException) 20 | { 21 | this.EventData = data; 22 | this.EventName = eventName; 23 | } 24 | 25 | /// 26 | /// Gets the data for the event action that raised the error. 27 | /// 28 | public TData EventData { get; private set; } 29 | 30 | /// 31 | /// Gets the event name for the action that raised an error. 32 | /// 33 | public string EventName { get; private set; } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /PusherClient/ChannelDataDecrypter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using NaCl; 4 | 5 | namespace PusherClient 6 | { 7 | internal class ChannelDataDecrypter : IChannelDataDecrypter 8 | { 9 | public string DecryptData(byte[] decryptionKey, EncryptedChannelData encryptedData) 10 | { 11 | string decryptedText = null; 12 | byte[] cipher = null; 13 | byte[] nonce = null; 14 | if (encryptedData != null) 15 | { 16 | if (encryptedData.ciphertext != null) 17 | { 18 | cipher = Convert.FromBase64String(encryptedData.ciphertext); 19 | } 20 | 21 | if (encryptedData.nonce != null) 22 | { 23 | nonce = Convert.FromBase64String(encryptedData.nonce); 24 | } 25 | } 26 | 27 | if (cipher != null && nonce != null) 28 | { 29 | using (XSalsa20Poly1305 secretBox = new XSalsa20Poly1305(decryptionKey)) 30 | { 31 | byte[] decryptedBytes = new byte[cipher.Length - XSalsa20Poly1305.TagLength]; 32 | if (secretBox.TryDecrypt(decryptedBytes, cipher, nonce)) 33 | { 34 | decryptedText = Encoding.UTF8.GetString(decryptedBytes); 35 | } 36 | else 37 | { 38 | throw new ChannelDecryptionException("Decryption failed for channel."); 39 | } 40 | } 41 | } 42 | else 43 | { 44 | throw new ChannelDecryptionException("Insufficient data received; requires encrypted data with 'ciphertext' and 'nonce'."); 45 | } 46 | 47 | return decryptedText; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /PusherClient.Tests.Utilities/EnvironmentVariableConfigLoader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient.Tests.Utilities 4 | { 5 | /// 6 | /// Loads configuration from system environment variables. 7 | /// 8 | public class EnvironmentVariableConfigLoader : IApplicationConfigLoader 9 | { 10 | private const string PUSHER_APP_ID = "PUSHER_APP_ID"; 11 | private const string PUSHER_APP_KEY = "PUSHER_APP_KEY"; 12 | private const string PUSHER_APP_SECRET = "PUSHER_APP_SECRET"; 13 | private const string PUSHER_APP_CLUSTER = "PUSHER_APP_CLUSTER"; 14 | private const string PUSHER_APP_ENCRYPTED = "PUSHER_APP_ENCRYPTED"; 15 | 16 | /// 17 | /// Gets a static default instance of this class. 18 | /// 19 | public static IApplicationConfigLoader Default { get; } = new EnvironmentVariableConfigLoader(); 20 | 21 | /// 22 | /// Loads configuration from system environment variables. 23 | /// 24 | /// An instance. 25 | public IApplicationConfig Load() 26 | { 27 | ApplicationConfig result = new ApplicationConfig 28 | { 29 | AppId = Environment.GetEnvironmentVariable(PUSHER_APP_ID), 30 | AppKey = Environment.GetEnvironmentVariable(PUSHER_APP_KEY), 31 | AppSecret = Environment.GetEnvironmentVariable(PUSHER_APP_SECRET), 32 | Cluster = Environment.GetEnvironmentVariable(PUSHER_APP_CLUSTER), 33 | }; 34 | string encryptedText = Environment.GetEnvironmentVariable(PUSHER_APP_ENCRYPTED); 35 | if ("true".Equals(encryptedText, StringComparison.OrdinalIgnoreCase)) 36 | { 37 | result.Encrypted = true; 38 | } 39 | 40 | return result; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /PusherClient.Tests/UnitTests/ChannelDataDecrypterTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using PusherClient.Tests.AcceptanceTests; 3 | 4 | namespace PusherClient.Tests.UnitTests 5 | { 6 | [TestFixture] 7 | public class ChannelDataDecrypterTest 8 | { 9 | private static readonly byte[] EncryptionMasterKey = EventEmitterTest.GenerateEncryptionMasterKey(); 10 | 11 | [Test] 12 | public void ChannelDataDecrypterWillRaiseErrorWhenEncryptedDataIsNull() 13 | { 14 | // Arrange 15 | ChannelDecryptionException error = null; 16 | ChannelDataDecrypter decrypter = new ChannelDataDecrypter(); 17 | string keyText = System.Convert.ToBase64String(EncryptionMasterKey); 18 | Assert.IsNotNull(keyText); 19 | 20 | // Act 21 | try 22 | { 23 | decrypter.DecryptData(EncryptionMasterKey, null); 24 | } 25 | catch(ChannelDecryptionException exception) 26 | { 27 | error = exception; 28 | } 29 | 30 | // Assert 31 | Assert.IsNotNull(error, $"Expected a {nameof(ChannelDecryptionException)} exception"); 32 | } 33 | 34 | [Test] 35 | public void ChannelDataDecrypterWillRaiseErrorWhenEncryptedDataPropertiesAreNotProvided() 36 | { 37 | // Arrange 38 | ChannelDecryptionException error = null; 39 | ChannelDataDecrypter decrypter = new ChannelDataDecrypter(); 40 | EncryptedChannelData data = new EncryptedChannelData(); 41 | 42 | // Act 43 | try 44 | { 45 | decrypter.DecryptData(EncryptionMasterKey, data); 46 | } 47 | catch (ChannelDecryptionException exception) 48 | { 49 | error = exception; 50 | } 51 | 52 | // Assert 53 | Assert.IsNotNull(error, $"Expected a {nameof(ChannelDecryptionException)} exception"); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /PusherClient/PusherClient.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | net45;net472;netstandard1.3;netstandard2.0 7 | true 8 | 2.0.0 9 | false 10 | 11 | 12 | 13 | ..\PusherClient.snk 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 4.3.0 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /PusherClient.Tests.Utilities/LatencyInducer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace PusherClient.Tests.Utilities 5 | { 6 | /// 7 | /// A latency inducer. 8 | /// 9 | public class LatencyInducer : ILatencyInducer 10 | { 11 | /// 12 | /// Gets or sets whether this latency inducer is enabled. 13 | /// 14 | public bool Enabled { get; set; } = Config.EnableAuthorizationLatency; 15 | 16 | /// 17 | /// If enabled, pauses for a random period between and . 18 | /// 19 | /// The minimum latency to induce measured in milli-seconds. 20 | /// The maximum latency to induce measured in milli-seconds. 21 | /// The number of milli-seconds that was induced. Will return 0 if disabled. 22 | public int InduceLatency(int minLatency, int maxLatency) 23 | { 24 | return InduceLatencyAsync(minLatency, maxLatency).Result; 25 | } 26 | 27 | /// 28 | /// If enabled, pauses for a random period between and . 29 | /// 30 | /// The minimum latency to induce measured in milli-seconds. 31 | /// The maximum latency to induce measured in milli-seconds. 32 | /// A task that can be awaited on. 33 | public async Task InduceLatencyAsync(int minLatency, int maxLatency) 34 | { 35 | int result = 0; 36 | if (this.Enabled) 37 | { 38 | TimeSpan pause = TimeSpan.FromMilliseconds(Random.Next(minLatency, maxLatency + 1)); 39 | result = pause.Milliseconds; 40 | await Task.Delay(pause).ConfigureAwait(false); 41 | } 42 | 43 | return result; 44 | } 45 | 46 | private static Random Random { get; } = new Random(42); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /PusherClient/Exception/UserAuthenticationFailureException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient 4 | { 5 | /// 6 | /// This exception is raised when calling Authenticate or AuthenticateAsync on the . 7 | /// 8 | public class UserAuthenticationFailureException : PusherException 9 | { 10 | /// 11 | /// Creates a new instance of a class. 12 | /// 13 | /// The exception message. 14 | /// The Pusher error code. 15 | /// The user authentication endpoint URL. 16 | /// The socket ID used in the user authentication attempt. 17 | public UserAuthenticationFailureException(string message, ErrorCodes code, string authenticationEndpoint, string socketId) 18 | : base(message, code) 19 | { 20 | this.AuthenticationEndpoint = authenticationEndpoint; 21 | } 22 | 23 | /// 24 | /// Creates a new instance of a class. 25 | /// 26 | /// The exception message. 27 | /// The Pusher error code. 28 | /// The user authentication endpoint URL. 29 | /// The socket ID used in the user authentication attempt. 30 | /// The exception that caused the current exception. 31 | public UserAuthenticationFailureException(ErrorCodes code, string authenticationEndpoint, string socketId, Exception innerException) 32 | : base($"Error authenticating user {Environment.NewLine}{innerException.Message}", code, innerException) 33 | { 34 | this.AuthenticationEndpoint = authenticationEndpoint; 35 | } 36 | 37 | /// 38 | /// Gets or sets the authentication endpoint URL. 39 | /// 40 | public string AuthenticationEndpoint { get; private set; } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /PusherClient.Tests.Utilities/Config.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient.Tests.Utilities 2 | { 3 | /// 4 | /// Contains the default test configuration. 5 | /// 6 | public static class Config 7 | { 8 | static Config() 9 | { 10 | IApplicationConfig config = EnvironmentVariableConfigLoader.Default.Load(); 11 | if (string.IsNullOrWhiteSpace(config.AppKey)) 12 | { 13 | config = JsonFileConfigLoader.Default.Load(); 14 | } 15 | 16 | AppId = config.AppId; 17 | AppKey = config.AppKey; 18 | AppSecret = config.AppSecret; 19 | Cluster = config.Cluster; 20 | Encrypted = config.Encrypted; 21 | EnableAuthorizationLatency = config.EnableAuthorizationLatency ?? true; 22 | } 23 | 24 | /// 25 | /// Gets or sets the Pusher application id. 26 | /// 27 | public static string AppId { get; private set; } 28 | 29 | /// 30 | /// Gets or sets the Pusher application key. 31 | /// 32 | public static string AppKey { get; private set; } 33 | 34 | /// 35 | /// Gets or sets the Pusher application secret. 36 | /// 37 | public static string AppSecret { get; private set; } 38 | 39 | /// 40 | /// Gets or sets the Pusher application cluster. 41 | /// 42 | public static string Cluster { get; private set; } 43 | 44 | /// 45 | /// Gets or sets whether the connection will be encrypted. 46 | /// 47 | public static bool Encrypted { get; private set; } 48 | 49 | /// 50 | /// Gets or sets whether an artificial latency is induced when authorizing a channel. 51 | /// 52 | public static bool EnableAuthorizationLatency { get; set; } 53 | 54 | /// 55 | /// Gets the PusherServer HTTP host. 56 | /// 57 | public static string HttpHost 58 | { 59 | get 60 | { 61 | return $"api-{Cluster}.pusher.com"; 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /PusherClient/Exception/ChannelAuthorizationFailureException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient 4 | { 5 | /// 6 | /// This exception is raised when calling Authorize or AuthorizeAsync on the . 7 | /// 8 | public class ChannelAuthorizationFailureException : ChannelException 9 | { 10 | /// 11 | /// Creates a new instance of a class. 12 | /// 13 | /// The exception message. 14 | /// The Pusher error code. 15 | /// The authorization endpoint URL. 16 | /// The name of the channel. 17 | /// The socket ID used in the authorization attempt. 18 | public ChannelAuthorizationFailureException(string message, ErrorCodes code, string authorizationEndpoint, string channelName, string socketId) 19 | : base(message, code, channelName, socketId) 20 | { 21 | this.AuthorizationEndpoint = authorizationEndpoint; 22 | } 23 | 24 | /// 25 | /// Creates a new instance of a class. 26 | /// 27 | /// The exception message. 28 | /// The Pusher error code. 29 | /// The authorization endpoint URL. 30 | /// The name of the channel. 31 | /// The socket ID used in the authorization attempt. 32 | /// The exception that caused the current exception. 33 | public ChannelAuthorizationFailureException(ErrorCodes code, string authorizationEndpoint, string channelName, string socketId, Exception innerException) 34 | : base($"Error authorizing channel {channelName}:{Environment.NewLine}{innerException.Message}", code, channelName, socketId, innerException) 35 | { 36 | this.AuthorizationEndpoint = authorizationEndpoint; 37 | } 38 | 39 | /// 40 | /// Gets or sets the authorization endpoint URL. 41 | /// 42 | public string AuthorizationEndpoint { get; private set; } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /PusherClient/PusherEvent.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PusherClient 4 | { 5 | public class PusherEvent 6 | { 7 | private readonly Dictionary _eventData; 8 | private readonly string _rawEvent; 9 | 10 | public PusherEvent(Dictionary eventData, string rawEvent) 11 | { 12 | _eventData = eventData; 13 | _rawEvent = rawEvent; 14 | } 15 | 16 | public object GetProperty(string key) 17 | { 18 | _eventData.TryGetValue(key, out var value); 19 | return value; 20 | } 21 | 22 | public string UserId 23 | { 24 | get 25 | { 26 | string result = null; 27 | if (_eventData.TryGetValue("user_id", out object obj)) 28 | { 29 | result = obj.ToString(); 30 | } 31 | 32 | return result; 33 | } 34 | } 35 | 36 | public string ChannelName 37 | { 38 | get 39 | { 40 | string result = null; 41 | if (_eventData.TryGetValue("channel", out object obj)) 42 | { 43 | result = obj.ToString(); 44 | } 45 | 46 | return result; 47 | } 48 | } 49 | 50 | public string EventName 51 | { 52 | get 53 | { 54 | string result = null; 55 | if (_eventData.TryGetValue("event", out object obj)) 56 | { 57 | result = obj.ToString(); 58 | } 59 | 60 | return result; 61 | } 62 | } 63 | 64 | public string Data 65 | { 66 | get 67 | { 68 | string result = null; 69 | if (_eventData.TryGetValue("data", out object obj)) 70 | { 71 | if (obj is string) 72 | { 73 | result = (string)obj; 74 | } 75 | else 76 | { 77 | result = DefaultSerializer.Default.Serialize(obj); 78 | } 79 | } 80 | 81 | return result; 82 | } 83 | } 84 | 85 | public override string ToString() 86 | { 87 | return _rawEvent; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /PusherClient.Tests.Utilities/JsonFileConfigLoader.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Text; 5 | 6 | namespace PusherClient.Tests.Utilities 7 | { 8 | /// 9 | /// Loads test configuration from a json file. 10 | /// 11 | public class JsonFileConfigLoader : IApplicationConfigLoader 12 | { 13 | private const string DefaultFileName = "AppConfig.test.json"; 14 | 15 | /// 16 | /// Instantiates an instance of a using the default file name. 17 | /// 18 | public JsonFileConfigLoader() 19 | : this(Path.Combine(Assembly.GetExecutingAssembly().Location, $"../../../../../{DefaultFileName}")) 20 | { 21 | } 22 | 23 | /// 24 | /// Instantiates an instance of a using the specified . 25 | /// 26 | /// The full path to the Json config file. 27 | public JsonFileConfigLoader(string fileName) 28 | { 29 | this.FileName = fileName; 30 | } 31 | 32 | /// 33 | /// Gets a static default instance of this class. 34 | /// 35 | public static IApplicationConfigLoader Default { get; } = new JsonFileConfigLoader(); 36 | 37 | /// 38 | /// Gets or sets the Json config file name. 39 | /// 40 | public string FileName { get; set; } 41 | 42 | /// 43 | /// Loads test configuration from a Json file. 44 | /// 45 | /// An instance. 46 | /// Thrown when the Json settings file does not exist. 47 | public IApplicationConfig Load() 48 | { 49 | FileInfo fileInfo = new FileInfo(this.FileName); 50 | if (!fileInfo.Exists) 51 | { 52 | throw new FileNotFoundException($"The test application config file was not found at location '{fileInfo.FullName}'.", fileName: this.FileName); 53 | } 54 | 55 | string content = File.ReadAllText(fileInfo.FullName, Encoding.UTF8); 56 | ApplicationConfig result = JsonConvert.DeserializeObject(content); 57 | return result; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | pull_request: 5 | types: [ labeled ] 6 | branches: 7 | - master 8 | 9 | jobs: 10 | prepare-release: 11 | name: Prepare release 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Set major release 16 | if: ${{ github.event.label.name == 'release-major' }} 17 | run: echo "RELEASE=major" >> $GITHUB_ENV 18 | - name: Set minor release 19 | if: ${{ github.event.label.name == 'release-minor' }} 20 | run: echo "RELEASE=minor" >> $GITHUB_ENV 21 | - name: Set patch release 22 | if: ${{ github.event.label.name == 'release-patch' }} 23 | run: echo "RELEASE=patch" >> $GITHUB_ENV 24 | - name: Check release env 25 | run: | 26 | if [[ -z "${{ env.RELEASE }}" ]]; 27 | then 28 | echo "You need to set a release label on PRs to the main branch" 29 | exit 1 30 | else 31 | exit 0 32 | fi 33 | - name: Install semver-tool 34 | run: | 35 | export DIR=$(mtemp) 36 | cd $DIR 37 | curl https://github.com/fsaintjacques/semver-tool/archive/3.2.0.tar.gz -L -o semver.tar.gz 38 | tar -xvf semver.tar.gz 39 | sudo cp semver-tool-3.2.0/src/semver /usr/local/bin 40 | - name: Bump version 41 | run: | 42 | export CURRENT=$(nuget list packageid:PusherClient | grep PusherClient | cut -d' ' -f 2) 43 | export NEW_VERSION=$(semver bump ${{ env.RELEASE }} $CURRENT) 44 | echo "VERSION=$NEW_VERSION" >> $GITHUB_ENV 45 | - name: Checkout code 46 | uses: actions/checkout@v2 47 | - name: Setup git 48 | run: | 49 | git config user.email "pusher-ci@pusher.com" 50 | git config user.name "Pusher CI" 51 | git fetch 52 | git checkout ${{ github.event.pull_request.head.ref }} 53 | - name: Prepare package 54 | run: | 55 | sed -i 's/[^<]*<\/Version>/${{ env.VERSION }}<\/Version>/' Root.Build.props 56 | sed -i 's/[^<]*<\/AssemblyVersion>/${{ env.VERSION }}.0<\/AssemblyVersion>/' Root.Build.props 57 | sed -i 's/[^<]*<\/FileVersion>/${{ env.VERSION }}.0<\/FileVersion>/' Root.Build.props 58 | - name: Prepare CHANGELOG 59 | run: | 60 | echo "${{ github.event.pull_request.body }}" | csplit -s - "/##/" 61 | echo "# Changelog 62 | 63 | ## ${{ env.VERSION }}" >> CHANGELOG.tmp 64 | grep "^*" xx01 >> CHANGELOG.tmp 65 | grep -v "^# " CHANGELOG.md >> CHANGELOG.tmp 66 | cp CHANGELOG.tmp CHANGELOG.md 67 | git add Root.Build.props CHANGELOG.md 68 | git commit -m "Bump to version ${{ env.VERSION }}" 69 | - name: Push 70 | run: git push 71 | 72 | -------------------------------------------------------------------------------- /PusherClient.Tests.Utilities/PusherFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace PusherClient.Tests.Utilities 6 | { 7 | public static class PusherFactory 8 | { 9 | public static Pusher GetPusher(IAuthorizer authorizer = null, IList saveTo = null, int timeoutPeriodMilliseconds = 30 * 1000) 10 | { 11 | PusherOptions options = new PusherOptions() 12 | { 13 | Authorizer = authorizer, 14 | Cluster = Config.Cluster, 15 | Encrypted = Config.Encrypted, 16 | TraceLogger = new TraceLogger(), 17 | ClientTimeout = TimeSpan.FromMilliseconds(timeoutPeriodMilliseconds), 18 | }; 19 | 20 | Pusher result = new Pusher(Config.AppKey, options); 21 | result.Error += HandlePusherError; 22 | if (saveTo != null) 23 | { 24 | saveTo.Add(result); 25 | } 26 | 27 | return result; 28 | } 29 | 30 | public static Pusher GetPusher(ChannelTypes channelType, string username = null, IList saveTo = null, byte[] encryptionKey = null) 31 | { 32 | Pusher result; 33 | switch (channelType) 34 | { 35 | case ChannelTypes.Private: 36 | case ChannelTypes.Presence: 37 | result = GetPusher(new FakeAuthoriser(username ?? UserNameFactory.CreateUniqueUserName(), encryptionKey), saveTo: saveTo); 38 | break; 39 | 40 | default: 41 | result = GetPusher(authorizer: null, saveTo: saveTo); 42 | break; 43 | } 44 | 45 | return result; 46 | } 47 | 48 | public static async Task DisposePusherClientAsync(Pusher pusher) 49 | { 50 | if (pusher != null) 51 | { 52 | ((IPusher)pusher).PusherOptions.ClientTimeout = TimeSpan.FromSeconds(30); 53 | await pusher.UnsubscribeAllAsync().ConfigureAwait(false); 54 | await pusher.ConnectAsync().ConfigureAwait(false); 55 | await pusher.DisconnectAsync().ConfigureAwait(false); 56 | } 57 | } 58 | 59 | public static async Task DisposePushersAsync(IList pushers) 60 | { 61 | if (pushers != null) 62 | { 63 | for (int i = 0; i < pushers.Count; i++) 64 | { 65 | await DisposePusherClientAsync(pushers[i]).ConfigureAwait(false); 66 | pushers[i] = null; 67 | } 68 | 69 | pushers.Clear(); 70 | } 71 | } 72 | 73 | private static void HandlePusherError(object sender, PusherException error) 74 | { 75 | Pusher pusher = sender as Pusher; 76 | System.Diagnostics.Trace.TraceError($"Pusher error detected on socket {pusher.SocketID}:{Environment.NewLine}{error}"); 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /PusherClient/WatchlistFacade.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace PusherClient 9 | { 10 | internal class WatchlistFacade : EventEmitter, IWatchlistFacade 11 | { 12 | internal void OnPusherEvent(string eventName, PusherEvent pusherEvent) { 13 | if (eventName == Constants.PUSHER_WATCHLIST_EVENT) { 14 | List watchlistEvents = ParseWatchlistEvents(pusherEvent); 15 | foreach (WatchlistEvent watchlistEvent in watchlistEvents) { 16 | EmitEvent(watchlistEvent.Name, watchlistEvent); 17 | } 18 | } 19 | } 20 | 21 | List ParseWatchlistEvents(PusherEvent pusherEvent) { 22 | List watchlistEvents = new List(); 23 | JObject jObject = JObject.Parse(pusherEvent.Data); 24 | JToken jToken = jObject.SelectToken("events"); 25 | if (jToken != null) 26 | { 27 | if (jToken.Type == JTokenType.Array) 28 | { 29 | JArray eventsJsonArray = jToken.Value(); 30 | foreach (JToken eventJson in eventsJsonArray) { 31 | if (eventJson.Type == JTokenType.Object) 32 | { 33 | string name = null; 34 | List userIDs = null; 35 | JObject eventJsonObject = eventJson.Value(); 36 | JToken nameToken = eventJsonObject.SelectToken("name"); 37 | if (nameToken != null && nameToken.Type == JTokenType.String) 38 | { 39 | name = nameToken.Value(); 40 | } 41 | JToken userIDsToken = eventJsonObject.SelectToken("user_ids"); 42 | if (userIDsToken != null && userIDsToken.Type == JTokenType.Array) 43 | { 44 | userIDs = new List(); 45 | JArray userIDsArray = userIDsToken.Value(); 46 | foreach (JToken userIDToken in userIDsArray) { 47 | if (userIDToken.Type == JTokenType.String) 48 | { 49 | userIDs.Add(userIDToken.Value()); 50 | } 51 | } 52 | } 53 | 54 | if (name != null && userIDs != null) 55 | { 56 | watchlistEvents.Add(new WatchlistEvent(name, userIDs, eventJson.ToString())); 57 | } 58 | } 59 | } 60 | } 61 | } 62 | return watchlistEvents; 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /PusherClient.Tests.Utilities/FakeAuthoriser.cs: -------------------------------------------------------------------------------- 1 | using PusherServer; 2 | using System; 3 | using System.Diagnostics; 4 | using System.Threading.Tasks; 5 | 6 | namespace PusherClient.Tests.Utilities 7 | { 8 | public class FakeAuthoriser : IAuthorizer, IAuthorizerAsync 9 | { 10 | /// 11 | /// The minimum latency measured in milli-seconds. 12 | /// 13 | public const int MinLatency = 300; 14 | 15 | /// 16 | /// The maximum latency measured in milli-seconds. 17 | /// 18 | public const int MaxLatency = 1000; 19 | 20 | public const string TamperToken = "-tamper"; 21 | 22 | private readonly string _userName; 23 | 24 | private readonly byte[] _encryptionKey; 25 | 26 | public FakeAuthoriser() 27 | : this("Unknown") 28 | { 29 | } 30 | 31 | public FakeAuthoriser(string userName) 32 | : this(userName, null) 33 | { 34 | } 35 | 36 | public FakeAuthoriser(string userName, byte[] encryptionKey) 37 | { 38 | _userName = userName; 39 | _encryptionKey = encryptionKey; 40 | } 41 | 42 | public TimeSpan? Timeout { get; set; } 43 | 44 | public string Authorize(string channelName, string socketId) 45 | { 46 | return AuthorizeAsync(channelName, socketId).Result; 47 | } 48 | 49 | public async Task AuthorizeAsync(string channelName, string socketId) 50 | { 51 | string authData = null; 52 | double delay = (await LatencyInducer.InduceLatencyAsync(MinLatency, MaxLatency)) / 1000.0; 53 | Trace.TraceInformation($"{this.GetType().Name} paused for {Math.Round(delay, 3)} second(s)"); 54 | await Task.Run(() => 55 | { 56 | PusherServer.PusherOptions options = new PusherServer.PusherOptions 57 | { 58 | EncryptionMasterKey = _encryptionKey, 59 | Cluster = Config.Cluster, 60 | }; 61 | var provider = new PusherServer.Pusher(Config.AppId, Config.AppKey, Config.AppSecret, options); 62 | if (channelName.StartsWith("presence-")) 63 | { 64 | var channelData = new PresenceChannelData 65 | { 66 | user_id = socketId, 67 | user_info = new FakeUserInfo { name = _userName } 68 | }; 69 | 70 | authData = provider.Authenticate(channelName, socketId, channelData).ToJson(); 71 | } 72 | else 73 | { 74 | authData = provider.Authenticate(channelName, socketId).ToJson(); 75 | } 76 | 77 | if (channelName.Contains(TamperToken)) 78 | { 79 | authData = authData.Replace("1", "2"); 80 | } 81 | }).ConfigureAwait(false); 82 | return authData; 83 | } 84 | 85 | private static ILatencyInducer LatencyInducer { get; } = new LatencyInducer(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /PusherClient/EventHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PusherClient 4 | { 5 | /// 6 | /// The delegate to handle the Pusher.Connected event. 7 | /// 8 | /// The client that connected. 9 | public delegate void ConnectedEventHandler(object sender); 10 | 11 | /// 12 | /// The delegate to handle the Pusher.ConnectionStateChanged event. 13 | /// 14 | /// The client that's state changed. 15 | /// The new state. 16 | public delegate void ConnectionStateChangedEventHandler(object sender, ConnectionState state); 17 | 18 | /// 19 | /// The delegate to handle the Pusher.Error event. 20 | /// 21 | /// The client that errored. 22 | /// The error that occured. 23 | public delegate void ErrorEventHandler(object sender, PusherException error); 24 | 25 | /// 26 | /// The delegate for when a member is added to a or a . 27 | /// 28 | /// The detail of the member added. 29 | /// 30 | /// The or that had the member added. 31 | /// 32 | /// 33 | /// A where TKey is the user ID and TValue is the member detail added. 34 | /// 35 | public delegate void MemberAddedEventHandler(object sender, KeyValuePair member); 36 | 37 | /// 38 | /// The delegate for when a member is removed from a or a . 39 | /// 40 | /// The detail of the member removed. 41 | /// 42 | /// The or that had the member removed. 43 | /// 44 | /// 45 | /// A where TKey is the user ID and TValue is the member detail removed. 46 | /// 47 | public delegate void MemberRemovedEventHandler(object sender, KeyValuePair member); 48 | 49 | /// 50 | /// The delegate to handle the Pusher.Subscribed event. 51 | /// 52 | /// The client that has had a channel subcribed to. 53 | /// The channel that has been subscribed to. 54 | public delegate void SubscribedEventHandler(object sender, Channel channel); 55 | 56 | /// 57 | /// The delegate to handle the Channel.Subscribed event. 58 | /// 59 | /// The that subscribed 60 | /// 61 | /// To be deprecated, please use instead. 62 | /// 63 | public delegate void SubscriptionEventHandler(object sender); 64 | 65 | public delegate void SubscriptionCountHandler(object sender, string data); 66 | } 67 | -------------------------------------------------------------------------------- /PusherClient/Exception/ChannelException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient 4 | { 5 | /// 6 | /// This exception is raised when when a channel subscription error is detected. 7 | /// 8 | public class ChannelException : PusherException, IChannelException 9 | { 10 | /// 11 | /// Creates a new instance of a class. 12 | /// 13 | /// The exception message. 14 | /// The Pusher error code. 15 | /// The name of the channel. 16 | /// The socket ID used in the authorization attempt. 17 | public ChannelException(string message, ErrorCodes code, string channelName, string socketId) 18 | : base(message, code) 19 | { 20 | this.ChannelName = channelName; 21 | this.SocketID = socketId; 22 | } 23 | 24 | /// 25 | /// Creates a new instance of a class. 26 | /// 27 | /// The Pusher error code. 28 | /// The name of the channel. 29 | /// The socket ID used in the authorization attempt. 30 | /// The exception that caused the current exception. 31 | public ChannelException(ErrorCodes code, string channelName, string socketId, Exception innerException) 32 | : base($"Unexpected error subscribing to channel {channelName}:{Environment.NewLine}{innerException.Message}", code, innerException) 33 | { 34 | this.ChannelName = channelName; 35 | this.SocketID = socketId; 36 | } 37 | 38 | /// 39 | /// Creates a new instance of a class. 40 | /// 41 | /// The exception message. 42 | /// The Pusher error code. 43 | /// The name of the channel. 44 | /// The socket ID used in the authorization attempt. 45 | /// The exception that caused the current exception. 46 | public ChannelException(string message, ErrorCodes code, string channelName, string socketId, Exception innerException) 47 | : base(message, code, innerException) 48 | { 49 | this.ChannelName = channelName; 50 | this.SocketID = socketId; 51 | } 52 | 53 | /// 54 | /// Gets or sets the name of the channel for which the exception occured. 55 | /// 56 | public string ChannelName { get; set; } 57 | 58 | /// 59 | /// Gets or sets the event name for which the exception occured. Note that this property is not always available and can be null. 60 | /// 61 | public string EventName { get; set; } 62 | 63 | /// 64 | /// Gets or sets the channel's message data if available. 65 | /// 66 | public string MessageData { get; set; } 67 | 68 | /// 69 | /// Gets or sets the channel socket ID. 70 | /// 71 | public string SocketID { get; set; } 72 | 73 | /// 74 | /// Gets or sets the that errored if available. 75 | /// Note that this value can be null because the Channel object is not always available. 76 | /// 77 | public Channel Channel { get; set; } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /pusher-dotnet-client.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29009.5 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Files", "Files", "{2D9F07A0-C627-434A-8C94-CD7CD175720F}" 7 | ProjectSection(SolutionItems) = preProject 8 | .gitattributes = .gitattributes 9 | .gitignore = .gitignore 10 | AppConfig.sample.json = AppConfig.sample.json 11 | CHANGELOG.md = CHANGELOG.md 12 | CPU.count.2.runsettings = CPU.count.2.runsettings 13 | package.cmd = package.cmd 14 | README.md = README.md 15 | Root.Build.props = Root.Build.props 16 | EndProjectSection 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PusherClient", "PusherClient\PusherClient.csproj", "{A325BB9F-6476-4422-AEF4-C22FA53890DD}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AuthHost", "AuthHost\AuthHost.csproj", "{5199A7CF-2370-4A8E-9BFB-FB6722E68F2B}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PusherClient.Tests", "PusherClient.Tests\PusherClient.Tests.csproj", "{6CC1E65E-7F76-4A39-B07B-ADC9A7343A5A}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PusherClient.Tests.Utilities", "PusherClient.Tests.Utilities\PusherClient.Tests.Utilities.csproj", "{BAA85D91-CDB7-4ACF-A4F6-1EFBC5A9581F}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExampleApplication", "ExampleApplication\ExampleApplication.csproj", "{66F3E31F-6275-429F-81F9-319A3940E924}" 27 | EndProject 28 | Global 29 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 30 | Debug|Any CPU = Debug|Any CPU 31 | Release|Any CPU = Release|Any CPU 32 | EndGlobalSection 33 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 34 | {A325BB9F-6476-4422-AEF4-C22FA53890DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {A325BB9F-6476-4422-AEF4-C22FA53890DD}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {A325BB9F-6476-4422-AEF4-C22FA53890DD}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {A325BB9F-6476-4422-AEF4-C22FA53890DD}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {5199A7CF-2370-4A8E-9BFB-FB6722E68F2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {5199A7CF-2370-4A8E-9BFB-FB6722E68F2B}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {5199A7CF-2370-4A8E-9BFB-FB6722E68F2B}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {5199A7CF-2370-4A8E-9BFB-FB6722E68F2B}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {6CC1E65E-7F76-4A39-B07B-ADC9A7343A5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {6CC1E65E-7F76-4A39-B07B-ADC9A7343A5A}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {6CC1E65E-7F76-4A39-B07B-ADC9A7343A5A}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {6CC1E65E-7F76-4A39-B07B-ADC9A7343A5A}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {BAA85D91-CDB7-4ACF-A4F6-1EFBC5A9581F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {BAA85D91-CDB7-4ACF-A4F6-1EFBC5A9581F}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {BAA85D91-CDB7-4ACF-A4F6-1EFBC5A9581F}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {BAA85D91-CDB7-4ACF-A4F6-1EFBC5A9581F}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {66F3E31F-6275-429F-81F9-319A3940E924}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {66F3E31F-6275-429F-81F9-319A3940E924}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {66F3E31F-6275-429F-81F9-319A3940E924}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {66F3E31F-6275-429F-81F9-319A3940E924}.Release|Any CPU.Build.0 = Release|Any CPU 54 | EndGlobalSection 55 | GlobalSection(SolutionProperties) = preSolution 56 | HideSolutionNode = FALSE 57 | EndGlobalSection 58 | GlobalSection(ExtensibilityGlobals) = postSolution 59 | SolutionGuid = {4F56D497-EB8C-4666-8ED9-F66B79A64B7B} 60 | EndGlobalSection 61 | EndGlobal 62 | -------------------------------------------------------------------------------- /PusherClient.Tests/AcceptanceTests/SubscriptionTest.PrivateChannels.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using NUnit.Framework; 3 | using PusherClient.Tests.Utilities; 4 | 5 | namespace PusherClient.Tests.AcceptanceTests 6 | { 7 | public partial class SubscriptionTest 8 | { 9 | #region Connect then subscribe tests 10 | 11 | [Test] 12 | public async Task PrivateChannelConnectThenSubscribeAsync() 13 | { 14 | await ConnectThenSubscribeTestAsync(ChannelTypes.Private).ConfigureAwait(false); 15 | } 16 | 17 | [Test] 18 | public async Task PrivateChannelConnectThenSubscribeWithSubscribedErrorAsync() 19 | { 20 | await ConnectThenSubscribeTestAsync(ChannelTypes.Private, raiseSubscribedError: true).ConfigureAwait(false); 21 | } 22 | 23 | [Test] 24 | public async Task PrivateChannelConnectThenSubscribeTwiceAsync() 25 | { 26 | await ConnectThenSubscribeSameChannelTwiceAsync(ChannelTypes.Private).ConfigureAwait(false); 27 | } 28 | 29 | [Test] 30 | public async Task PrivateChannelConnectThenSubscribeMultipleTimesAsync() 31 | { 32 | await ConnectThenSubscribeSameChannelMultipleTimesTestAsync(ChannelTypes.Private).ConfigureAwait(false); 33 | } 34 | 35 | [Test] 36 | public async Task PrivateChannelConnectThenSubscribeWithoutAuthorizerAsync() 37 | { 38 | // Arrange 39 | var pusher = PusherFactory.GetPusher(saveTo: _clients); 40 | PusherException caughtException = null; 41 | 42 | // Act 43 | try 44 | { 45 | await ConnectThenSubscribeTestAsync(ChannelTypes.Private, pusher: pusher).ConfigureAwait(false); 46 | } 47 | catch (PusherException ex) 48 | { 49 | caughtException = ex; 50 | } 51 | 52 | // Assert 53 | Assert.IsNotNull(caughtException); 54 | StringAssert.Contains("A ChannelAuthorizer needs to be provided when subscribing to the private or presence channel", caughtException.Message); 55 | } 56 | 57 | #endregion 58 | 59 | #region Subscribe then connect tests 60 | 61 | [Test] 62 | public async Task PrivateChannelSubscribeThenConnectAsync() 63 | { 64 | await SubscribeThenConnectTestAsync(ChannelTypes.Private).ConfigureAwait(false); 65 | } 66 | 67 | [Test] 68 | public async Task PrivateChannelSubscribeThenConnectWithSubscribedErrorAsync() 69 | { 70 | await SubscribeThenConnectTestAsync(ChannelTypes.Private, raiseSubscribedError: true).ConfigureAwait(false); 71 | } 72 | 73 | [Test] 74 | public async Task PrivateChannelSubscribeTwiceThenConnectAsync() 75 | { 76 | await SubscribeThenConnectSameChannelTwiceAsync(ChannelTypes.Private).ConfigureAwait(false); 77 | } 78 | 79 | [Test] 80 | public async Task PrivateChannelSubscribeMultipleTimesThenConnectAsync() 81 | { 82 | await SubscribeThenConnectSameChannelMultipleTimesTestAsync(ChannelTypes.Private).ConfigureAwait(false); 83 | } 84 | 85 | #endregion 86 | 87 | #region No connection tests 88 | 89 | [Test] 90 | public async Task PrivateChannelSubscribeWithoutConnectingAsync() 91 | { 92 | await SubscribeWithoutConnectingTestAsync(ChannelTypes.Private).ConfigureAwait(false); 93 | } 94 | 95 | [Test] 96 | public async Task PrivateChannelSubscribeThenUnsubscribeWithoutConnectingAsync() 97 | { 98 | await SubscribeThenUnsubscribeWithoutConnectingTestAsync(ChannelTypes.Private).ConfigureAwait(false); 99 | } 100 | 101 | #endregion 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /PusherClient.Tests/AcceptanceTests/SubscriptionTest.PresenceChannels.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using NUnit.Framework; 3 | using PusherClient.Tests.Utilities; 4 | 5 | namespace PusherClient.Tests.AcceptanceTests 6 | { 7 | public partial class SubscriptionTest 8 | { 9 | #region Connect then subscribe tests 10 | 11 | [Test] 12 | public async Task PresenceChannelConnectThenSubscribeAsync() 13 | { 14 | await ConnectThenSubscribeTestAsync(ChannelTypes.Presence).ConfigureAwait(false); 15 | } 16 | 17 | [Test] 18 | public async Task PresenceChannelConnectThenSubscribeWithSubscribedErrorAsync() 19 | { 20 | await ConnectThenSubscribeTestAsync(ChannelTypes.Presence, raiseSubscribedError: true).ConfigureAwait(false); 21 | } 22 | 23 | [Test] 24 | public async Task PresenceChannelConnectThenSubscribeTwiceAsync() 25 | { 26 | await ConnectThenSubscribeSameChannelTwiceAsync(ChannelTypes.Presence).ConfigureAwait(false); 27 | } 28 | 29 | [Test] 30 | public async Task PresenceChannelConnectThenSubscribeMultipleTimesAsync() 31 | { 32 | await ConnectThenSubscribeSameChannelMultipleTimesTestAsync(ChannelTypes.Presence).ConfigureAwait(false); 33 | } 34 | 35 | [Test] 36 | public async Task PresenceChannelConnectThenSubscribeWithoutAuthorizerAsync() 37 | { 38 | // Arrange 39 | var pusher = PusherFactory.GetPusher(saveTo: _clients); 40 | PusherException caughtException = null; 41 | 42 | // Act 43 | try 44 | { 45 | await ConnectThenSubscribeTestAsync(ChannelTypes.Presence, pusher: pusher).ConfigureAwait(false); 46 | } 47 | catch (PusherException ex) 48 | { 49 | caughtException = ex; 50 | } 51 | 52 | // Assert 53 | Assert.IsNotNull(caughtException); 54 | StringAssert.Contains("A ChannelAuthorizer needs to be provided when subscribing to the private or presence channel", caughtException.Message); 55 | } 56 | 57 | #endregion 58 | 59 | #region Subscribe then connect tests 60 | 61 | [Test] 62 | public async Task PresenceChannelSubscribeThenConnectAsync() 63 | { 64 | await SubscribeThenConnectTestAsync(ChannelTypes.Presence).ConfigureAwait(false); 65 | } 66 | 67 | [Test] 68 | public async Task PresenceChannelSubscribeThenConnectWithSubscribedErrorAsync() 69 | { 70 | await SubscribeThenConnectTestAsync(ChannelTypes.Presence, raiseSubscribedError: true).ConfigureAwait(false); 71 | } 72 | 73 | [Test] 74 | public async Task PresenceChannelSubscribeTwiceThenConnectAsync() 75 | { 76 | await SubscribeThenConnectSameChannelTwiceAsync(ChannelTypes.Presence).ConfigureAwait(false); 77 | } 78 | 79 | [Test] 80 | public async Task PresenceChannelSubscribeMultipleTimesThenConnectAsync() 81 | { 82 | await SubscribeThenConnectSameChannelMultipleTimesTestAsync(ChannelTypes.Presence).ConfigureAwait(false); 83 | } 84 | 85 | #endregion 86 | 87 | #region No connection tests 88 | 89 | [Test] 90 | public async Task PresenceChannelSubscribeWithoutConnectingAsync() 91 | { 92 | await SubscribeWithoutConnectingTestAsync(ChannelTypes.Presence).ConfigureAwait(false); 93 | } 94 | 95 | [Test] 96 | public async Task PresenceChannelSubscribeThenUnsubscribeWithoutConnectingAsync() 97 | { 98 | await SubscribeThenUnsubscribeWithoutConnectingTestAsync(ChannelTypes.Presence).ConfigureAwait(false); 99 | } 100 | 101 | #endregion 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /PusherClient/PusherOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherClient 4 | { 5 | /// 6 | /// The Options to set up the connection with 7 | /// 8 | public class PusherOptions 9 | { 10 | private string _cluster; 11 | private string _host; 12 | 13 | /// 14 | /// Instantiates an instance of a object. 15 | /// 16 | public PusherOptions() 17 | { 18 | Cluster = "mt1"; 19 | } 20 | 21 | /// 22 | /// Gets or sets whether the connection will be encrypted. 23 | /// 24 | public bool Encrypted { get; set; } 25 | 26 | /// 27 | /// DEPRECATED: Use instead. 28 | /// Gets or set the to use. 29 | /// 30 | public IAuthorizer Authorizer { 31 | get 32 | { 33 | return (IAuthorizer) ChannelAuthorizer; 34 | } 35 | set 36 | { 37 | ChannelAuthorizer = value; 38 | } 39 | } 40 | 41 | /// 42 | /// Gets or set the to use. 43 | /// 44 | public IChannelAuthorizer ChannelAuthorizer { get; set; } = null; 45 | 46 | /// 47 | /// Gets or set the to use. 48 | /// 49 | public IUserAuthenticator UserAuthenticator { get; set; } = null; 50 | 51 | /// 52 | /// Gets or sets the cluster to use for the host. 53 | /// 54 | public string Cluster 55 | { 56 | get 57 | { 58 | return _cluster; 59 | } 60 | 61 | set 62 | { 63 | if (_cluster != value) 64 | { 65 | _cluster = value; 66 | if (_cluster != null) 67 | { 68 | _host = $"ws-{_cluster}.pusher.com"; 69 | } 70 | else 71 | { 72 | _host = null; 73 | } 74 | } 75 | } 76 | } 77 | 78 | /// 79 | /// Gets or sets the the host to use. For example; ws-some-server:8086. 80 | /// 81 | /// 82 | /// This value will override the Cluster property. This property should only be used in advanced scenarios. 83 | /// Use the Cluster property instead to define the Pusher server host. 84 | /// 85 | public string Host 86 | { 87 | get 88 | { 89 | return _host; 90 | } 91 | 92 | set 93 | { 94 | if (_host != value) 95 | { 96 | _host = value; 97 | _cluster = null; 98 | } 99 | } 100 | } 101 | 102 | /// 103 | /// Gets or sets the timeout period to wait for an asynchrounous operation to complete. The default value is 30 seconds. 104 | /// 105 | public TimeSpan ClientTimeout { get; set; } = TimeSpan.FromSeconds(30); 106 | 107 | /// 108 | /// Gets or sets the to use for tracing debug messages. 109 | /// 110 | public ITraceLogger TraceLogger { get; set; } 111 | 112 | /// 113 | /// Gets a timeout 10% less than ClientTimeout. This value is used for inner timeouts. 114 | /// 115 | internal TimeSpan InnerClientTimeout 116 | { 117 | get 118 | { 119 | return TimeSpan.FromTicks(ClientTimeout.Ticks - ClientTimeout.Ticks / 10); 120 | } 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /PusherClient.Tests/UnitTests/PusherEventTest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using NUnit.Framework; 3 | using System.Collections.Generic; 4 | 5 | namespace PusherClient.Tests.UnitTests 6 | { 7 | [TestFixture] 8 | public class PusherEventTest 9 | { 10 | [Test] 11 | public void PusherEventShouldPopulateCorrectlyWhenGivenAnEventDataDictionaryAndRawJsonString() 12 | { 13 | // Arrange 14 | var testData = GetTestData("cunning user name","a channel","Event name"); 15 | var rawJson = JsonConvert.SerializeObject(testData); 16 | 17 | // Act 18 | var pusherEvent = CreateTestEvent(testData); 19 | 20 | // Assert 21 | Assert.AreEqual("cunning user name", pusherEvent.UserId); 22 | Assert.AreEqual("a channel", pusherEvent.ChannelName); 23 | Assert.AreEqual("Event name", pusherEvent.EventName); 24 | Assert.AreEqual("stuff", pusherEvent.Data); 25 | Assert.AreEqual(rawJson, pusherEvent.ToString()); 26 | } 27 | 28 | [Test] 29 | public void PusherEventShouldRetrieveAnAdditionalPropertyThatIsNotAvailableAsPropertyOnItself() 30 | { 31 | var testData = GetExtendedTestData("cunning user name", "a channel", "Event name"); 32 | 33 | // Act 34 | var pusherEvent = CreateTestEvent(testData); 35 | 36 | // Assert 37 | Assert.AreEqual("cunning user name", pusherEvent.UserId); 38 | Assert.AreEqual("a channel", pusherEvent.ChannelName); 39 | Assert.AreEqual("Event name", pusherEvent.EventName); 40 | Assert.AreEqual("more stuff", pusherEvent.Data); 41 | Assert.AreEqual("an extra property", pusherEvent.GetProperty("ExtraProperty").ToString()); 42 | } 43 | 44 | [Test] 45 | public void PusherEventShouldReturnNullWhenAskedToRetrieveAPropertyThatIsNotAvailableAsPropertyOnItself() 46 | { 47 | var testData = GetTestData("cunning user name", "a channel", "Event name"); 48 | 49 | // Act 50 | var pusherEvent = CreateTestEvent(testData); 51 | 52 | // Assert 53 | Assert.AreEqual("cunning user name", pusherEvent.UserId); 54 | Assert.AreEqual("a channel", pusherEvent.ChannelName); 55 | Assert.AreEqual("Event name", pusherEvent.EventName); 56 | Assert.AreEqual("stuff", pusherEvent.Data); 57 | Assert.IsNull(pusherEvent.GetProperty("ExtraProperty")); 58 | } 59 | 60 | private PusherClient.PusherEvent CreateTestEvent(TestClass testClass) 61 | { 62 | var raw = JsonConvert.SerializeObject(testClass); 63 | var properties = JsonConvert.DeserializeObject>(raw); 64 | 65 | var template = new { @event = string.Empty, data = string.Empty, channel = string.Empty }; 66 | 67 | var message = JsonConvert.DeserializeAnonymousType(raw, template); 68 | 69 | 70 | return new PusherClient.PusherEvent(properties, raw); 71 | } 72 | 73 | private TestClass GetTestData(string username, string channel, string eventName) 74 | { 75 | return new TestClass 76 | { 77 | user_id = username, 78 | channel = channel, 79 | @event = eventName, 80 | data = "stuff" 81 | }; 82 | } 83 | 84 | private ExtendedTestClass GetExtendedTestData(string username, string channel, string eventName) 85 | { 86 | return new ExtendedTestClass 87 | { 88 | user_id = username, 89 | channel = channel, 90 | @event = eventName, 91 | data = "more stuff", 92 | ExtraProperty = "an extra property" 93 | }; 94 | } 95 | 96 | private class TestClass 97 | { 98 | public string user_id { get; set; } 99 | public string channel { get; set; } 100 | public string @event { get; set; } 101 | public string data { get; set; } 102 | } 103 | 104 | private class ExtendedTestClass : TestClass 105 | { 106 | public string ExtraProperty { get; set; } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /AuthHost/AuthModule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Nancy; 4 | using Newtonsoft.Json; 5 | using PusherClient; 6 | using PusherClient.Tests.Utilities; 7 | using PusherServer; 8 | 9 | namespace AuthHost 10 | { 11 | public class AuthModule : NancyModule 12 | { 13 | 14 | public string PusherApplicationKey => Config.AppKey; 15 | 16 | public string PusherApplicationId => Config.AppId; 17 | 18 | public string PusherApplicationSecret => Config.AppSecret; 19 | 20 | public const string EncryptionMasterKeyText = "Rk4twMwEogcmx5dpV+6puT+nNidXoRd3smLvWR57FbQ="; 21 | 22 | public AuthModule() 23 | { 24 | PusherServer.PusherOptions options = new PusherServer.PusherOptions 25 | { 26 | EncryptionMasterKey = Convert.FromBase64String(EncryptionMasterKeyText), 27 | Cluster = Config.Cluster, 28 | }; 29 | var provider = new PusherServer.Pusher(PusherApplicationId, PusherApplicationKey, PusherApplicationSecret, options); 30 | 31 | Post["/auth/{username}", ctx => ctx.Request.Form.channel_name && ctx.Request.Form.socket_id] = _ => 32 | { 33 | Console.WriteLine($"Processing auth request for '{Request.Form.channel_name}' channel, for socket ID '{Request.Form.socket_id}'"); 34 | 35 | string channelName = Request.Form.channel_name; 36 | string socketId = Request.Form.socket_id; 37 | 38 | string authData = null; 39 | 40 | if (Channel.GetChannelType(channelName) == ChannelTypes.Presence) 41 | { 42 | var channelData = new PresenceChannelData 43 | { 44 | user_id = socketId, 45 | user_info = new { Name = _.username.ToString() } 46 | }; 47 | 48 | authData = provider.Authenticate(channelName, socketId, channelData).ToJson(); 49 | } 50 | else 51 | { 52 | authData = provider.Authenticate(channelName, socketId).ToJson(); 53 | } 54 | 55 | if (Channel.GetChannelType(channelName) == ChannelTypes.PrivateEncrypted) 56 | { 57 | #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed 58 | SendSecretMessageAsync(); 59 | #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed 60 | } 61 | 62 | return authData; 63 | }; 64 | 65 | // Add routes that ExampleApplication expects 66 | Post["/pusher/auth"] = _ => 67 | { 68 | Console.WriteLine($"Processing auth request for '{Request.Form.channel_name}' channel, for socket ID '{Request.Form.socket_id}'"); 69 | 70 | string channelName = Request.Form.channel_name; 71 | string socketId = Request.Form.socket_id; 72 | 73 | string authData = null; 74 | 75 | if (Channel.GetChannelType(channelName) == ChannelTypes.Presence) 76 | { 77 | var channelData = new PresenceChannelData 78 | { 79 | user_id = socketId, 80 | user_info = new { Name = "DefaultUser" } 81 | }; 82 | 83 | authData = provider.Authenticate(channelName, socketId, channelData).ToJson(); 84 | } 85 | else 86 | { 87 | authData = provider.Authenticate(channelName, socketId).ToJson(); 88 | } 89 | 90 | return authData; 91 | }; 92 | 93 | Post["/pusher/auth-user"] = _ => 94 | { 95 | Console.WriteLine($"Processing user auth request for socket ID '{Request.Form.socket_id}'"); 96 | 97 | string socketId = Request.Form.socket_id; 98 | 99 | // For user authentication, return a simple user auth response 100 | var userAuthResponse = new 101 | { 102 | auth = $"{PusherApplicationKey}:{socketId}", 103 | user_data = JsonConvert.SerializeObject(new { id = socketId, name = "DefaultUser" }) 104 | }; 105 | 106 | return JsonConvert.SerializeObject(userAuthResponse); 107 | }; 108 | } 109 | 110 | private async Task SendSecretMessageAsync() 111 | { 112 | await Task.Delay(5000).ConfigureAwait(false); 113 | PusherServer.PusherOptions options = new PusherServer.PusherOptions 114 | { 115 | EncryptionMasterKey = Convert.FromBase64String(EncryptionMasterKeyText), 116 | Cluster = Config.Cluster, 117 | }; 118 | string channelName = "private-encrypted-channel"; 119 | string eventName = "secret-event"; 120 | var provider = new PusherServer.Pusher(PusherApplicationId, PusherApplicationKey, PusherApplicationSecret, options); 121 | string secretMessage = $"sent secret at {DateTime.Now} on '{channelName}' using event '{eventName}'."; 122 | await provider.TriggerAsync(channelName, eventName, new 123 | { 124 | Name = nameof(AuthModule), 125 | Message = secretMessage, 126 | }).ConfigureAwait(false); 127 | Console.WriteLine(secretMessage); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /PusherClient/GenericPresenceChannel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using Newtonsoft.Json; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace PusherClient 8 | { 9 | /// 10 | /// Represents a Pusher Presence Channel that can be subscribed to. 11 | /// 12 | /// Type used to deserialize channel member detail. 13 | public class GenericPresenceChannel : PrivateChannel, IPresenceChannel, IPresenceChannelManagement 14 | { 15 | /// 16 | /// Fires when a Member is Added 17 | /// 18 | public event MemberAddedEventHandler MemberAdded; 19 | 20 | /// 21 | /// Fires when a Member is Removed 22 | /// 23 | public event MemberRemovedEventHandler MemberRemoved; 24 | 25 | internal GenericPresenceChannel(string channelName, ITriggerChannels pusher) : base(channelName, pusher) 26 | { 27 | } 28 | 29 | /// 30 | /// Gets the Members of the channel 31 | /// 32 | private ConcurrentDictionary Members { get; set; } = new ConcurrentDictionary(); 33 | 34 | /// 35 | /// Gets a member using the member's user ID. 36 | /// 37 | /// The member's user ID. 38 | /// Retruns the member if found; otherwise returns null. 39 | public T GetMember(string userId) 40 | { 41 | T result = default; 42 | if (Members.TryGetValue(userId, out T member)) 43 | { 44 | result = member; 45 | } 46 | 47 | return result; 48 | } 49 | 50 | /// 51 | /// Gets the current list of members as a where the TKey is the user ID and TValue is the member detail. 52 | /// 53 | /// Returns a containing the current members. 54 | public Dictionary GetMembers() 55 | { 56 | Dictionary result = new Dictionary(Members.Count); 57 | foreach (var member in Members) 58 | { 59 | result.Add(member.Key, member.Value); 60 | } 61 | 62 | return result; 63 | } 64 | 65 | internal override void SubscriptionSucceeded(string data) 66 | { 67 | if (!IsSubscribed) 68 | { 69 | Members = ParseMembersList(data); 70 | base.SubscriptionSucceeded(data); 71 | } 72 | } 73 | 74 | void IPresenceChannelManagement.AddMember(string data) 75 | { 76 | var member = ParseMember(data); 77 | Members[member.Key] = member.Value; 78 | if (MemberAdded != null) 79 | { 80 | try 81 | { 82 | MemberAdded.Invoke(this, member); 83 | } 84 | catch(Exception e) 85 | { 86 | _pusher.RaiseChannelError(new MemberAddedEventHandlerException(member.Key, member.Value, e)); 87 | } 88 | } 89 | } 90 | 91 | void IPresenceChannelManagement.RemoveMember(string data) 92 | { 93 | var parsedMember = ParseMember(data); 94 | if (Members.TryRemove(parsedMember.Key, out T member)) 95 | { 96 | if (MemberRemoved != null) 97 | { 98 | try 99 | { 100 | MemberRemoved.Invoke(this, new KeyValuePair(parsedMember.Key, member)); 101 | } 102 | catch (Exception e) 103 | { 104 | _pusher.RaiseChannelError(new MemberRemovedEventHandlerException(parsedMember.Key, member, e)); 105 | } 106 | } 107 | } 108 | } 109 | 110 | private class SubscriptionData 111 | { 112 | public Presence presence { get; set; } 113 | 114 | internal class Presence 115 | { 116 | public List ids { get; set; } 117 | public Dictionary hash { get; set; } 118 | } 119 | } 120 | 121 | private ConcurrentDictionary ParseMembersList(string data) 122 | { 123 | JToken jToken = JToken.Parse(data); 124 | JObject jObject = JObject.Parse(jToken.ToString()); 125 | 126 | ConcurrentDictionary members = new ConcurrentDictionary(); 127 | 128 | var dataAsObj = JsonConvert.DeserializeObject(jObject.ToString(Formatting.None)); 129 | 130 | for (int i = 0; i < dataAsObj.presence.ids.Count; i++) 131 | { 132 | string id = dataAsObj.presence.ids[i]; 133 | T val = dataAsObj.presence.hash[id]; 134 | members[id] = val; 135 | } 136 | 137 | return members; 138 | } 139 | 140 | private class MemberData 141 | { 142 | public string user_id { get; set; } 143 | public T user_info { get; set; } 144 | } 145 | 146 | private KeyValuePair ParseMember(string data) 147 | { 148 | var dataAsObj = JsonConvert.DeserializeObject(data); 149 | 150 | var id = dataAsObj.user_id; 151 | var val = dataAsObj.user_info; 152 | 153 | return new KeyValuePair(id, val); 154 | } 155 | } 156 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.3.1 4 | * [CHANGED] Fixes TLS support by forcing TLS 1.2 5 | 6 | ## 2.3.0-beta 7 | * [Added] Introduce user features 8 | 9 | ## 2.2.1 10 | * [CHANGED] Update dependency Newtonsoft.Json 13.0.2 11 | 12 | ## 2.2.0 13 | * [Added] Support for subscription_count handler 14 | 15 | ## 2.1.0 16 | * [ADDED] Strong name to the PusherClient assembly. 17 | * [ADDED] Support for the authentication header on the HttpAuthorizer. 18 | * [ADDED] End-to-end encryption for private encrypted channels. 19 | * [ADDED] Method Channel.UnsubscribeAsync. 20 | * [ADDED] Host to PusherOptions. 21 | * [FIXED] The intermittent WebsocketAutoReconnect issue The socket is connecting, cannot connect again! 22 | 23 | ## 2.0.1 24 | * [FIXED] Filter on event name in event emitter. 25 | 26 | ## 2.0.0 27 | * [FIXED] Infinite loop when failing to connect for the first time. 28 | * [FIXED] Bug: GenericPresenceChannel's AddMember and RemoveMember events were not being emitted. 29 | * [FIXED] Change MemberRemovedEventHandler to MemberRemovedEventHandler. 30 | * [FIXED] Introduce new ChannelUnauthorizedException class. 31 | * [FIXED] Bug: calling Channel.Unsubscribe would only set IsSubscribed to false and not remove the channel subscription. 32 | * [FIXED] Bug: Pusher can error if any of the delegate events raise errors. 33 | * [FIXED] Bug: PusherEvent.Data fails to serialize for types other than string. 34 | * [FIXED] Concurrency issues in EventEmmiter. 35 | * [FIXED] Failing tests for Pusher apps in a cluster other than the default. 36 | * [FIXED] Issues in the Example app and removed the use of dynamic types. 37 | * [CHANGED] PusherClient project structure to target .NET 4.5, .NET 4.7.2, .NET Standard 1.3 and .NET Standard 2.0. 38 | * [CHANGED] The events emitted by Pusher.ConnectionStateChanged. Connecting and Connected are new. Initialized has been removed. Disconnecting, Disconnected and WaitingToReconnect remain unchanged. 39 | * [CHANGED] Pusher methods ConnectAsync and DisconnectAsync to return void instead of ConnectionState. 40 | * [CHANGED] Pusher.SubscribeAsync to take an optional SubscriptionEventHandler parameter. 41 | * [CHANGED] Separate PusherEvent, string and dynamic emitters into separate classes. 42 | * [CHANGED] Bump PusherServer version to 4.4.0. 43 | * [CHANGED] PusherClient project structure to target .NET 4.5, .NET 4.7.2, .NET Standard 1.3 and .NET Standard 2.0. 44 | * [REMOVED] public property Pusher.Channels; it is now private. 45 | * [REMOVED] public property GenericPresenceChannel.Members; it is now private. 46 | * [REMOVED] The following ConnectionState values: Initialized, NotConnected, AlreadyConnected, ConnectionFailed and DisconnectionFailed. 47 | * [REMOVED] public Channel.Subscribed; it is now internal. 48 | * [REMOVED] Pusher.Trace property. 49 | * [ADDED] To Pusher class: methods UnsubscribeAsync, UnsubscribeAllAsync, GetChannel and GetAllChannels; event delegate Subscribed. 50 | * [ADDED] To GenericPresenceChannel class: methods GetMember and GetMembers. 51 | * [ADDED] ClientTimeout to PusherOptions and implemented client timeouts with tests. 52 | * [ADDED] Json config file for test application settings. 53 | * [ADDED] Client side event triggering validation. 54 | * [ADDED] ITraceLogger interface and TraceLogger class, tracing is disabled by default. New TraceLogger property added to PusherOptions. 55 | 56 | ## 1.1.2 57 | * [FIX] Switch to concurrent collections to avoid race in EventEmitter (issue #76, PR #93) 58 | * [FIX] Fix Reconnection issue and NRE on Disconnect() after websocket_Closed (issue #70, issue #71, issue #73, PR #95) 59 | * [FIX] Reset `_backOffMillis` after a successful reconnection (issue #97, PR #96) 60 | 61 | ## 1.1.1 62 | * [FIX] Removed extra double quotes from PusherEvent.Data string (PR #84) 63 | * [FIX] Fixed JsonReaderException in the HttpAuthorizer (issue #78, issue #85, PR #86) 64 | 65 | ## 1.1.0 66 | * [FIX] Mitigates NRE and race in Connect/Disconnect (PR #72, issue #71) 67 | * [FIX] Potential incompatibility with il2cpp compiler for iOS target due to dynamic keyword (issue #69) 68 | * [FIX] Race condition in PresenceChannel name (issue #44) 69 | * [ADDED] Extended API for Bind/Unbind and BindAll/UnbindAll to emit a more idiomatic as an alternative to the raw data String 70 | * [ADDED] NUnit 3 Test Adaptor to enable test integration with VS2019 71 | 72 | ## 1.0.2 73 | * [CHANGED] Project now targets DotNet Standard 1.6. 74 | * [REMOVED] Retired Sync versions of method. Updated tests to use async versions of methods. 75 | 76 | ## 0.5.1 77 | * [FIXED] Potential crash when a channel is added while a connection state change is being handled. 78 | * [CHANGED] Log an error rather than throw an exception if an error occurs on a connection. 79 | 80 | ## 0.5.0 81 | * [CHANGED] After calling Connect(), subsequent calls will no-op unless Disconnect() is called first. 82 | * [CHANGED] Event listeners are removed from _websocket and _connection on disconnect 83 | 84 | ## 0.4.0 85 | * [CHANGED] MemberAdded event handler is now aware of *which* member has been added 86 | * [ADDED] Unbind methods 87 | * [ADDED] Ability to change cluster (thanks @Misiu) 88 | * [CHANGED] Update dependencies 89 | * Newtonsoft.Json 9.0.1 90 | * WebSocket4Net 0.14.1 91 | 92 | ## 0.3.0 93 | 94 | * [ADDED] Auto re-subscription 95 | * [ADDED] Exceptions are raised via an event and aren't thrown (unless there are no event handlers) 96 | * [ADDED] Auto-reconnections now backs off by 1 second increments to a maximum of a retry every 10 seconds. 97 | * [ADDED] The WebSocket Host can be configured 98 | * [ADDED] You can now subscribe prior to connecting 99 | * [REMOVED] `Pusher.Send` method on the public API 100 | * [CHANGED] Update dependencies 101 | * Newtonsoft.Json 7.0.1 102 | * WebSocket4Net 0.13.1 103 | 104 | ## 0.2.0 105 | 106 | * [CHANGED] Update package dependencies 107 | * Newtonsoft.Json 6.0.4 108 | * WebSocket4Net 0.10 109 | -------------------------------------------------------------------------------- /PusherClient/HttpUserAuthenticator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Net.Http.Headers; 6 | using System.Threading.Tasks; 7 | 8 | namespace PusherClient 9 | { 10 | /// 11 | /// An implementation of the using Http 12 | /// 13 | public class HttpUserAuthenticator: IUserAuthenticator, IUserAuthenticatorAsync 14 | { 15 | private readonly Uri _authEndpoint; 16 | 17 | /// 18 | /// ctor 19 | /// 20 | /// The End point to contact 21 | public HttpUserAuthenticator(string authEndpoint) 22 | { 23 | _authEndpoint = new Uri(authEndpoint); 24 | } 25 | 26 | /// 27 | /// Gets or sets the internal authentication header. 28 | /// 29 | public AuthenticationHeaderValue AuthenticationHeader { get; set; } 30 | 31 | /// 32 | /// Gets or sets the timeout period for the authenticator. If not specified, the default timeout of 100 seconds is used. 33 | /// 34 | public TimeSpan? Timeout { get; set; } 35 | 36 | /// 37 | /// Perform the authentication of a user. 38 | /// 39 | /// The socket ID to use. 40 | /// If authentication fails or if an HTTP call to the authentication URL fails; that is, the HTTP status code is outside of the range 200-299. 41 | /// The response received from the authentication endpoint. 42 | public string Authenticate(string socketId) 43 | { 44 | string result; 45 | try 46 | { 47 | result = AuthenticateAsync(socketId).Result; 48 | } 49 | catch(AggregateException aggregateException) 50 | { 51 | throw aggregateException.InnerException; 52 | } 53 | 54 | return result; 55 | } 56 | 57 | /// 58 | /// Perform the authentication of a user. 59 | /// 60 | /// The socket ID to use. 61 | /// If authentication fails or if an HTTP call to the authentication URL fails; that is, the HTTP status code is outside of the range 200-299. 62 | /// The response received from the authentication endpoint. 63 | public async Task AuthenticateAsync(string socketId) 64 | { 65 | Console.WriteLine("Authenticating..."); 66 | string authToken = null; 67 | using (var httpClient = new HttpClient()) 68 | { 69 | var data = new List> 70 | { 71 | new KeyValuePair("socket_id", socketId), 72 | }; 73 | 74 | using (HttpContent content = new FormUrlEncodedContent(data)) 75 | { 76 | HttpResponseMessage response = null; 77 | try 78 | { 79 | PreRequest(httpClient); 80 | response = await httpClient.PostAsync(_authEndpoint, content).ConfigureAwait(false); 81 | } 82 | catch (Exception e) 83 | { 84 | ErrorCodes code = ErrorCodes.UserAuthenticationError; 85 | if (e is TaskCanceledException) 86 | { 87 | code = ErrorCodes.UserAuthenticationTimeout; 88 | } 89 | 90 | throw new UserAuthenticationFailureException(code, _authEndpoint.OriginalString, socketId, e); 91 | } 92 | 93 | if (response.StatusCode == HttpStatusCode.RequestTimeout || response.StatusCode == HttpStatusCode.GatewayTimeout) 94 | { 95 | throw new UserAuthenticationFailureException($"Authentication timeout ({response.StatusCode}).", ErrorCodes.UserAuthenticationTimeout, _authEndpoint.OriginalString, socketId); 96 | } 97 | else if (response.StatusCode == HttpStatusCode.Forbidden) 98 | { 99 | throw new UserAuthenticationFailureException($"Authentication Failed ({response.StatusCode}).", ErrorCodes.UserAuthenticationError, _authEndpoint.OriginalString, socketId); 100 | } 101 | 102 | try 103 | { 104 | response.EnsureSuccessStatusCode(); 105 | authToken = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 106 | } 107 | catch(Exception e) 108 | { 109 | throw new UserAuthenticationFailureException(ErrorCodes.UserAuthenticationError, _authEndpoint.OriginalString, socketId, e); 110 | } 111 | } 112 | } 113 | 114 | return authToken; 115 | } 116 | 117 | /// 118 | /// Called before submitting the request to the authentication endpoint. 119 | /// 120 | /// The used to submit the authentication request. 121 | public virtual void PreRequest(HttpClient httpClient) 122 | { 123 | if (Timeout.HasValue) 124 | { 125 | httpClient.Timeout = Timeout.Value; 126 | } 127 | 128 | if (AuthenticationHeader != null) 129 | { 130 | httpClient.DefaultRequestHeaders.Authorization = AuthenticationHeader; 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /PusherClient.Tests/AcceptanceTests/SubscriptionTest.PublicChannels.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using NUnit.Framework; 3 | using PusherClient.Tests.Utilities; 4 | using Newtonsoft.Json; 5 | 6 | namespace PusherClient.Tests.AcceptanceTests 7 | { 8 | /// 9 | /// Tests subscribe and unsubscribe functionality for Public channels. 10 | /// 11 | [TestFixture] 12 | public partial class SubscriptionTest 13 | { 14 | #region Connect then subscribe tests 15 | 16 | [Test] 17 | public async Task PublicChannelConnectThenSubscribeAsync() 18 | { 19 | await ConnectThenSubscribeTestAsync(ChannelTypes.Public).ConfigureAwait(false); 20 | } 21 | 22 | [Test] 23 | public async Task PublicChannelConnectThenSubscribeWithSubscribedErrorAsync() 24 | { 25 | await ConnectThenSubscribeTestAsync(ChannelTypes.Public, raiseSubscribedError: true).ConfigureAwait(false); 26 | } 27 | 28 | [Test] 29 | public async Task PublicChannelConnectThenSubscribeTwiceAsync() 30 | { 31 | await ConnectThenSubscribeSameChannelTwiceAsync(ChannelTypes.Public).ConfigureAwait(false); 32 | } 33 | 34 | [Test] 35 | public async Task PublicChannelConnectThenSubscribeMultipleTimesAsync() 36 | { 37 | await ConnectThenSubscribeSameChannelMultipleTimesTestAsync(ChannelTypes.Public).ConfigureAwait(false); 38 | } 39 | 40 | [Test] 41 | public async Task PublicChannelConnectThenSubscribeWithoutAnyEventHandlersAsync() 42 | { 43 | // Arrange 44 | var pusher = PusherFactory.GetPusher(saveTo: _clients); 45 | var mockChannelName = ChannelNameFactory.CreateUniqueChannelName(); 46 | 47 | // Act 48 | await pusher.ConnectAsync().ConfigureAwait(false); 49 | var channel = await pusher.SubscribeAsync(mockChannelName).ConfigureAwait(false); 50 | 51 | // Assert 52 | ValidateSubscribedChannel(pusher, mockChannelName, channel, ChannelTypes.Public); 53 | } 54 | 55 | [Test] 56 | public async Task PublicChannelSubscribeAndRecieveCountEvent() { 57 | var definition = new { subscription_count = 1 }; 58 | var pusher = PusherFactory.GetPusher(saveTo: _clients); 59 | 60 | void PusherCountEventHandler(object sender, string data) { 61 | var dataAsObj = JsonConvert.DeserializeAnonymousType(data, definition); 62 | Assert.Equals(dataAsObj.subscription_count, 1); 63 | } 64 | 65 | pusher.CountHandler += PusherCountEventHandler; 66 | await ConnectThenSubscribeTestAsync(ChannelTypes.Public, pusher: pusher); 67 | } 68 | 69 | [Test] 70 | public async Task PublicChannelUnsubscribeUsingChannelUnsubscribeAsync() 71 | { 72 | // Arrange 73 | var pusher = PusherFactory.GetPusher(saveTo: _clients); 74 | var mockChannelName = ChannelNameFactory.CreateUniqueChannelName(); 75 | 76 | // Act 77 | await pusher.ConnectAsync().ConfigureAwait(false); 78 | var channel = await pusher.SubscribeAsync(mockChannelName).ConfigureAwait(false); 79 | channel.Unsubscribe(); 80 | 81 | // Assert 82 | ValidateUnsubscribedChannel(pusher, channel); 83 | } 84 | 85 | [Test] 86 | public async Task PublicChannelUnsubscribeUsingChannelUnsubscribeDeadlockBugAsync() 87 | { 88 | // Arrange 89 | var pusher = PusherFactory.GetPusher(saveTo: _clients); 90 | var mockChannelName = ChannelNameFactory.CreateUniqueChannelName(); 91 | 92 | // Act 93 | await pusher.ConnectAsync().ConfigureAwait(false); 94 | var channel = await pusher.SubscribeAsync(mockChannelName).ConfigureAwait(false); 95 | 96 | // Assert 97 | ValidateSubscribedChannel(pusher, mockChannelName, channel, ChannelTypes.Public); 98 | 99 | await DeadlockBugShutdown(channel, pusher); 100 | } 101 | 102 | #endregion 103 | 104 | #region Subscribe then connect tests 105 | 106 | [Test] 107 | public async Task PublicChannelSubscribeThenConnectAsync() 108 | { 109 | await SubscribeThenConnectTestAsync(ChannelTypes.Public).ConfigureAwait(false); 110 | } 111 | 112 | [Test] 113 | public async Task PublicChannelSubscribeThenConnectWithSubscribedErrorAsync() 114 | { 115 | await SubscribeThenConnectTestAsync(ChannelTypes.Public, raiseSubscribedError: true).ConfigureAwait(false); 116 | } 117 | 118 | [Test] 119 | public async Task PublicChannelSubscribeTwiceThenConnectAsync() 120 | { 121 | await SubscribeThenConnectSameChannelTwiceAsync(ChannelTypes.Public).ConfigureAwait(false); 122 | } 123 | 124 | [Test] 125 | public async Task PublicChannelSubscribeMultipleTimesThenConnectAsync() 126 | { 127 | await SubscribeThenConnectSameChannelMultipleTimesTestAsync(ChannelTypes.Public).ConfigureAwait(false); 128 | } 129 | 130 | #endregion 131 | 132 | #region No connection tests 133 | 134 | [Test] 135 | public async Task PublicChannelSubscribeWithoutConnectingAsync() 136 | { 137 | await SubscribeWithoutConnectingTestAsync(ChannelTypes.Public).ConfigureAwait(false); 138 | } 139 | 140 | [Test] 141 | public async Task PublicChannelSubscribeThenUnsubscribeWithoutConnectingAsync() 142 | { 143 | await SubscribeThenUnsubscribeWithoutConnectingTestAsync(ChannelTypes.Public).ConfigureAwait(false); 144 | } 145 | 146 | #endregion 147 | 148 | #region Helper methods 149 | 150 | private Task DeadlockBugShutdown(Channel channel, Pusher pusherClient) 151 | { 152 | channel.UnbindAll(); 153 | channel.Unsubscribe(); 154 | return pusherClient.DisconnectAsync(); 155 | } 156 | 157 | #endregion 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /PusherClient/HttpChannelAuthorizer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Net.Http.Headers; 6 | using System.Threading.Tasks; 7 | 8 | namespace PusherClient 9 | { 10 | /// 11 | /// An implementation of the using Http 12 | /// 13 | public class HttpChannelAuthorizer: IChannelAuthorizer, IChannelAuthorizerAsync 14 | { 15 | private readonly Uri _authEndpoint; 16 | 17 | /// 18 | /// ctor 19 | /// 20 | /// The End point to contact 21 | public HttpChannelAuthorizer(string authEndpoint) 22 | { 23 | _authEndpoint = new Uri(authEndpoint); 24 | } 25 | 26 | /// 27 | /// Gets or sets the internal authentication header. 28 | /// 29 | public AuthenticationHeaderValue AuthenticationHeader { get; set; } 30 | 31 | /// 32 | /// Gets or sets the timeout period for the authorizer. If not specified, the default timeout of 100 seconds is used. 33 | /// 34 | public TimeSpan? Timeout { get; set; } 35 | 36 | /// 37 | /// Perform the authorization of the channel. 38 | /// 39 | /// The name of the channel to authorise on. 40 | /// The socket ID to use. 41 | /// If is null or whitespace. 42 | /// If authorization fails. 43 | /// If an HTTP call to the authorization URL fails; that is, the HTTP status code is outside of the range 200-299. 44 | /// The response received from the authorization endpoint. 45 | public string Authorize(string channelName, string socketId) 46 | { 47 | string result; 48 | try 49 | { 50 | result = AuthorizeAsync(channelName, socketId).Result; 51 | } 52 | catch(AggregateException aggregateException) 53 | { 54 | throw aggregateException.InnerException; 55 | } 56 | 57 | return result; 58 | } 59 | 60 | /// 61 | /// Perform the authorization of the channel. 62 | /// 63 | /// The name of the channel to authorise on. 64 | /// The socket ID to use. 65 | /// If is null or whitespace. 66 | /// If authorization fails. 67 | /// If an HTTP call to the authorization URL fails; that is, the HTTP status code is outside of the range 200-299. 68 | /// The response received from the authorization endpoint. 69 | public async Task AuthorizeAsync(string channelName, string socketId) 70 | { 71 | Guard.ChannelName(channelName); 72 | 73 | string authToken = null; 74 | using (var httpClient = new HttpClient()) 75 | { 76 | var data = new List> 77 | { 78 | new KeyValuePair("channel_name", channelName), 79 | new KeyValuePair("socket_id", socketId), 80 | }; 81 | 82 | using (HttpContent content = new FormUrlEncodedContent(data)) 83 | { 84 | HttpResponseMessage response = null; 85 | try 86 | { 87 | PreAuthorize(httpClient); 88 | response = await httpClient.PostAsync(_authEndpoint, content).ConfigureAwait(false); 89 | } 90 | catch (Exception e) 91 | { 92 | ErrorCodes code = ErrorCodes.ChannelAuthorizationError; 93 | if (e is TaskCanceledException) 94 | { 95 | code = ErrorCodes.ChannelAuthorizationTimeout; 96 | } 97 | 98 | throw new ChannelAuthorizationFailureException(code, _authEndpoint.OriginalString, channelName, socketId, e); 99 | } 100 | 101 | if (response.StatusCode == HttpStatusCode.RequestTimeout || response.StatusCode == HttpStatusCode.GatewayTimeout) 102 | { 103 | throw new ChannelAuthorizationFailureException($"Authorization timeout ({response.StatusCode}).", ErrorCodes.ChannelAuthorizationTimeout, _authEndpoint.OriginalString, channelName, socketId); 104 | } 105 | else if (response.StatusCode == HttpStatusCode.Forbidden) 106 | { 107 | throw new ChannelUnauthorizedException(_authEndpoint.OriginalString, channelName, socketId); 108 | } 109 | 110 | try 111 | { 112 | response.EnsureSuccessStatusCode(); 113 | authToken = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 114 | } 115 | catch(Exception e) 116 | { 117 | throw new ChannelAuthorizationFailureException(ErrorCodes.ChannelAuthorizationError, _authEndpoint.OriginalString, channelName, socketId, e); 118 | } 119 | } 120 | } 121 | 122 | return authToken; 123 | } 124 | 125 | /// 126 | /// Called before submitting the request to the authorization endpoint. 127 | /// 128 | /// The used to submit the authorization request. 129 | public virtual void PreAuthorize(HttpClient httpClient) 130 | { 131 | if (Timeout.HasValue) 132 | { 133 | httpClient.Timeout = Timeout.Value; 134 | } 135 | 136 | if (AuthenticationHeader != null) 137 | { 138 | httpClient.DefaultRequestHeaders.Authorization = AuthenticationHeader; 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /PusherClient/ErrorCodes.cs: -------------------------------------------------------------------------------- 1 | namespace PusherClient 2 | { 3 | /// 4 | /// An enum representing the different error codes. Errors 4000 - 4999 are received back from the Pusher cluster. 5 | /// Error codes 5000 and above are for errors detected in the client. 6 | /// 7 | public enum ErrorCodes 8 | { 9 | /// 10 | /// Unknown error. 11 | /// 12 | Unknown = 0, 13 | 14 | /// 15 | /// The connection must be established over SSL. 16 | /// 17 | MustConnectOverSSL = 4000, 18 | 19 | /// 20 | /// The application does not exist. 21 | /// 22 | ApplicationDoesNotExist = 4001, 23 | 24 | /// 25 | /// The application has been disbaled. 26 | /// 27 | ApplicationDisabled = 4003, 28 | 29 | /// 30 | /// The application has exceeded its connection quota. 31 | /// 32 | ApplicationOverConnectionQuota = 4004, 33 | 34 | /// 35 | /// The path was not found. 36 | /// 37 | PathNotFound = 4005, 38 | 39 | /// 40 | /// Connection not authorized within timeout. 41 | /// 42 | ConnectionNotAuthorizedWithinTimeout = 4009, 43 | 44 | /// 45 | /// The client has exceeded its rate limit. 46 | /// 47 | ClientOverRateLimit = 4301, 48 | 49 | /// 50 | /// The client has timed out waiting for an asynchronous operation to complete. 51 | /// 52 | ClientTimeout = 5000, 53 | 54 | /// 55 | /// An unexpected error has been detected when trying to connect. 56 | /// 57 | ConnectError = 5001, 58 | 59 | /// 60 | /// An unexpected error has been detected when trying to disconnect. 61 | /// 62 | DisconnectError = 5002, 63 | 64 | /// 65 | /// A subscription error has occured. 66 | /// 67 | SubscriptionError = 5003, 68 | 69 | /// 70 | /// An unexpected error has been detected when trying to receive a message from the Pusher server. 71 | /// 72 | MessageReceivedError = 5004, 73 | 74 | /// 75 | /// An unexpected error has been detected when trying to reconnect the web socket. 76 | /// 77 | ReconnectError = 5005, 78 | 79 | /// 80 | /// A WebSocket4Net.WebSocket error has occured. 81 | /// 82 | WebSocketError = 5006, 83 | 84 | /// 85 | /// An event emitter action error has occured. 86 | /// 87 | EventEmitterActionError = 5100, 88 | 89 | /// 90 | /// Attempt to trigger an event with an event name that does not begin with 'client-'. 91 | /// 92 | TriggerEventNameInvalidError = 5101, 93 | 94 | /// 95 | /// Attempt to trigger an event when not connected. 96 | /// 97 | TriggerEventNotConnectedError = 5102, 98 | 99 | /// 100 | /// Attempt to trigger an event when not subscribed. 101 | /// 102 | TriggerEventNotSubscribedError = 5103, 103 | 104 | /// 105 | /// Attempt to trigger an event using a public channel. 106 | /// 107 | TriggerEventPublicChannelError = 5104, 108 | 109 | /// 110 | /// Attempt to trigger an event using a private encrypted channel. 111 | /// 112 | TriggerEventPrivateEncryptedChannelError = 5105, 113 | 114 | /// 115 | /// The presence or private channel has not had its Authorizer set. 116 | /// 117 | ChannelAuthorizerNotSet = 7500, 118 | 119 | /// 120 | /// An error was caught when attempting to authorize a presence or private channel. For example; a 404 Not Found HTTP error was raised by the Authorizer. 121 | /// 122 | ChannelAuthorizationError = 7501, 123 | 124 | /// 125 | /// A timeout error was caught when attempting to authorize a presence or private channel. 126 | /// 127 | ChannelAuthorizationTimeout = 7502, 128 | 129 | /// 130 | /// The presence or private channel is unauthorized. Received a 403 Forbidden HTTP error from the Authorizer. 131 | /// 132 | ChannelUnauthorized = 7503, 133 | 134 | /// 135 | /// The presence channel is already defined using a different member type. 136 | /// 137 | PresenceChannelAlreadyDefined = 7504, 138 | 139 | /// 140 | /// The data for a private encrypted channel could not be decrypted. 141 | /// 142 | ChannelDecryptionFailure = 7505, 143 | 144 | /// 145 | /// An error was caught when emitting an event to the Pusher.Connected event handler. 146 | /// 147 | ConnectedEventHandlerError = 7601, 148 | 149 | /// 150 | /// An error was caught when emitting an event to the Pusher.ConnectionStateChanged event handler. 151 | /// 152 | ConnectionStateChangedEventHandlerError = 7602, 153 | 154 | /// 155 | /// An error was caught when emitting an event to the Pusher.Disconnected event handler. 156 | /// 157 | DisconnectedEventHandlerError = 7603, 158 | 159 | /// 160 | /// An error was caught when emitting an event to the GenericPresenceChannel.MemberAdded event handler. 161 | /// 162 | MemberAddedEventHandlerError = 7604, 163 | 164 | /// 165 | /// An error was caught when emitting an event to the GenericPresenceChannel.MemberRemoved event handler. 166 | /// 167 | MemberRemovedEventHandlerError = 7605, 168 | 169 | /// 170 | /// An error was caught when emitting an event to the Pusher.Subscribed or Channel.Subscribed event handlers. 171 | /// 172 | SubscribedEventHandlerError = 7606, 173 | 174 | /// 175 | /// An error was caught when attempting to authenticate a user. For example; a 404 Not Found HTTP error was raised by the UserAuthenticator. 176 | /// 177 | UserAuthenticationError = 8000, 178 | 179 | /// 180 | /// A timeout error was caught when attempting to authenticate a user. 181 | /// 182 | UserAuthenticationTimeout = 8001, 183 | } 184 | } -------------------------------------------------------------------------------- /PusherClient/Channel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Newtonsoft.Json; 5 | 6 | namespace PusherClient 7 | { 8 | /// 9 | /// Represents a Pusher channel that can be subscribed to 10 | /// 11 | public class Channel : EventEmitter 12 | { 13 | internal readonly ITriggerChannels _pusher; 14 | internal SemaphoreSlim _subscribeLock = new SemaphoreSlim(1); 15 | internal SemaphoreSlim _subscribeCompleted; 16 | internal Exception _subscriptionError; 17 | 18 | private bool _isSubscribed; 19 | 20 | /// 21 | /// Fired when the Channel has successfully been subscribed to. 22 | /// 23 | internal event SubscriptionEventHandler Subscribed; 24 | internal event SubscriptionCountHandler CountHandler; 25 | 26 | public int SubscriptionCount = 0; 27 | 28 | /// 29 | /// Gets whether the Channel is currently Subscribed 30 | /// 31 | public bool IsSubscribed 32 | { 33 | get 34 | { 35 | return _isSubscribed; 36 | } 37 | 38 | internal set 39 | { 40 | _isSubscribed = value; 41 | if (value) 42 | { 43 | IsServerSubscribed = true; 44 | } 45 | } 46 | } 47 | 48 | /// 49 | /// Gets the name of the Channel 50 | /// 51 | public string Name { get; } 52 | 53 | /// 54 | /// Gets the channel type; Public, Private or Presence. 55 | /// 56 | public ChannelTypes ChannelType 57 | { 58 | get 59 | { 60 | return GetChannelType(this.Name); 61 | } 62 | } 63 | 64 | /// 65 | /// Gets whether the channel ever received a pusher_internal:subscription_succeeded message. 66 | /// 67 | internal bool IsServerSubscribed { get; set; } 68 | 69 | /// 70 | /// Ctor 71 | /// 72 | /// The name of the Channel 73 | /// The parent Pusher object 74 | internal Channel(string channelName, ITriggerChannels pusher) 75 | { 76 | _pusher = pusher; 77 | Name = channelName; 78 | SetEventEmitterErrorHandler(pusher.RaiseChannelError); 79 | } 80 | 81 | internal virtual void SubscriptionSucceeded(string data) 82 | { 83 | if (!IsSubscribed) 84 | { 85 | IsSubscribed = true; 86 | if (Subscribed != null) 87 | { 88 | try 89 | { 90 | Subscribed.Invoke(this); 91 | } 92 | catch (Exception error) 93 | { 94 | _pusher.RaiseChannelError(new SubscribedEventHandlerException(this, error, data)); 95 | } 96 | } 97 | } 98 | } 99 | 100 | internal virtual void SubscriberCount(string data) 101 | { SubscriptionCount = ParseCount(data); 102 | if (CountHandler != null) 103 | { 104 | try 105 | { 106 | CountHandler.Invoke(this, data); 107 | } 108 | catch (Exception error) 109 | { 110 | _pusher.RaiseChannelError(new SubscribedEventHandlerException(this, error, data)); 111 | } 112 | } 113 | } 114 | 115 | /// 116 | /// Removes the channel subscription. 117 | /// 118 | public void Unsubscribe() 119 | { 120 | Task.WaitAll(UnsubscribeAsync()); 121 | } 122 | 123 | /// 124 | /// Removes the channel subscription. 125 | /// 126 | /// An awaitable . 127 | public async Task UnsubscribeAsync() 128 | { 129 | await _pusher.ChannelUnsubscribeAsync(Name).ConfigureAwait(false); 130 | } 131 | 132 | /// 133 | /// Trigger this channel with the provided information 134 | /// 135 | /// The name of the event to trigger 136 | /// The object to send as the payload on the event 137 | public void Trigger(string eventName, object obj) 138 | { 139 | Guard.EventName(eventName); 140 | Task.WaitAll(_pusher.TriggerAsync(Name, eventName, obj)); 141 | } 142 | 143 | /// 144 | /// Trigger this channel with the provided information 145 | /// 146 | /// The name of the event to trigger 147 | /// The object to send as the payload on the event 148 | /// An awaitable Task. 149 | public async Task TriggerAsync(string eventName, object obj) 150 | { 151 | Guard.EventName(eventName); 152 | await _pusher.TriggerAsync(Name, eventName, obj).ConfigureAwait(false); 153 | } 154 | 155 | /// 156 | /// Derives the channel type from the channel name. 157 | /// 158 | /// The channel name 159 | /// The channel type; Public, Private or Presence. 160 | public static ChannelTypes GetChannelType(string channelName) 161 | { 162 | Guard.ChannelName(channelName); 163 | 164 | ChannelTypes channelType = ChannelTypes.Public; 165 | if (channelName.StartsWith(Constants.PRIVATE_ENCRYPTED_CHANNEL, StringComparison.OrdinalIgnoreCase)) 166 | { 167 | channelType = ChannelTypes.PrivateEncrypted; 168 | } 169 | else if (channelName.StartsWith(Constants.PRIVATE_CHANNEL, StringComparison.OrdinalIgnoreCase)) 170 | { 171 | channelType = ChannelTypes.Private; 172 | } 173 | else if (channelName.StartsWith(Constants.PRESENCE_CHANNEL, StringComparison.OrdinalIgnoreCase)) 174 | { 175 | channelType = ChannelTypes.Presence; 176 | } 177 | 178 | return channelType; 179 | } 180 | 181 | public class SubscriptionCountData 182 | { 183 | [JsonProperty("subscription_count")] 184 | public int subscriptionCount { get; set; } 185 | } 186 | 187 | private int ParseCount(string data) 188 | { 189 | var dataAsObj = JsonConvert.DeserializeObject(data); 190 | return dataAsObj.subscriptionCount; 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /PusherClient/EventEmitter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace PusherClient 5 | { 6 | /// 7 | /// Class used to Bind and unbind from events 8 | /// 9 | public class EventEmitter 10 | { 11 | internal static readonly string[] EmitterKeys = new string[] 12 | { 13 | nameof(PusherEventEmitter), 14 | nameof(TextEventEmitter), 15 | nameof(DynamicEventEmitter), 16 | }; 17 | 18 | private IDictionary Emitters { get; } = new SortedList 19 | { 20 | {nameof(PusherEventEmitter), new PusherEventEmitter() }, 21 | {nameof(TextEventEmitter), new TextEventEmitter() }, 22 | {nameof(DynamicEventEmitter), new DynamicEventEmitter() }, 23 | }; 24 | 25 | /// 26 | /// Binds to a given event name. 27 | /// 28 | /// The Event Name to listen for. 29 | /// The action to perform when the event occurs. 30 | public void Bind(string eventName, Action listener) 31 | { 32 | ((DynamicEventEmitter)Emitters[nameof(DynamicEventEmitter)]).Bind(eventName, listener); 33 | } 34 | 35 | /// 36 | /// Binds to a given event name. The listener will receive the raw JSON message. 37 | /// 38 | /// The Event Name to listen for. 39 | /// The action to perform when the event occurs. 40 | public void Bind(string eventName, Action listener) 41 | { 42 | ((TextEventEmitter)Emitters[nameof(TextEventEmitter)]).Bind(eventName, listener); 43 | } 44 | 45 | /// 46 | /// Binds to a given event name. The listener will receive a Pusher Event. 47 | /// 48 | /// The Event Name to listen for. 49 | /// The action to perform when the event occurs. 50 | public void Bind(string eventName, Action listener) 51 | { 52 | ((PusherEventEmitter)Emitters[nameof(PusherEventEmitter)]).Bind(eventName, listener); 53 | } 54 | 55 | /// 56 | /// Binds a listener that listens to all events. 57 | /// 58 | /// The action to perform when the any event occurs. 59 | public void BindAll(Action listener) 60 | { 61 | ((DynamicEventEmitter)Emitters[nameof(DynamicEventEmitter)]).Bind(listener); 62 | } 63 | 64 | /// 65 | /// Binds a listener that listens to all events. 66 | /// The listener will receive the raw JSON message. 67 | /// 68 | /// The action to perform when the any event occurs. 69 | public void BindAll(Action listener) 70 | { 71 | ((TextEventEmitter)Emitters[nameof(TextEventEmitter)]).Bind(listener); 72 | } 73 | 74 | /// 75 | /// Binds a listener that listens to all events. The listener will receive a . 76 | /// 77 | /// The action to perform when the any event occurs. 78 | public void BindAll(Action listener) 79 | { 80 | ((PusherEventEmitter)Emitters[nameof(PusherEventEmitter)]).Bind(listener); 81 | } 82 | 83 | /// 84 | /// Removes the binding for the given event name 85 | /// 86 | /// The name of the event to unbind 87 | public void Unbind(string eventName) 88 | { 89 | foreach (IEventBinder binder in Emitters.Values) 90 | { 91 | binder.Unbind(eventName); 92 | } 93 | } 94 | 95 | /// 96 | /// Remove the action for the event name 97 | /// 98 | /// The name of the event to unbind 99 | /// The action to remove 100 | public void Unbind(string eventName, Action listener) 101 | { 102 | ((DynamicEventEmitter)Emitters[nameof(DynamicEventEmitter)]).Unbind(eventName, listener); 103 | } 104 | 105 | /// 106 | /// Remove the action for the event name 107 | /// 108 | /// The name of the event to unbind 109 | /// The action to remove 110 | public void Unbind(string eventName, Action listener) 111 | { 112 | ((TextEventEmitter)Emitters[nameof(TextEventEmitter)]).Unbind(eventName, listener); 113 | } 114 | 115 | /// 116 | /// Remove the action for the event name 117 | /// 118 | /// The name of the event to unbind 119 | /// The action to remove 120 | public void Unbind(string eventName, Action listener) 121 | { 122 | ((PusherEventEmitter)Emitters[nameof(PusherEventEmitter)]).Unbind(eventName, listener); 123 | } 124 | 125 | /// 126 | /// Unbinds a listener that listens to all events. 127 | /// 128 | /// The listener to unbind. 129 | public void Unbind(Action listener) 130 | { 131 | ((DynamicEventEmitter)Emitters[nameof(DynamicEventEmitter)]).Unbind(listener); 132 | } 133 | 134 | /// 135 | /// Unbinds a listener that listens to all events. 136 | /// 137 | /// The listener to unbind. 138 | public void Unbind(Action listener) 139 | { 140 | ((TextEventEmitter)Emitters[nameof(TextEventEmitter)]).Unbind(listener); 141 | } 142 | 143 | /// 144 | /// Unbinds a listener that listens to all events. 145 | /// 146 | /// The listener to unbind. 147 | public void Unbind(Action listener) 148 | { 149 | ((PusherEventEmitter)Emitters[nameof(PusherEventEmitter)]).Unbind(listener); 150 | } 151 | 152 | /// 153 | /// Remove All bindings 154 | /// 155 | public void UnbindAll() 156 | { 157 | foreach (IEventBinder binder in Emitters.Values) 158 | { 159 | binder.UnbindAll(); 160 | } 161 | } 162 | 163 | internal IEventBinder GetEventBinder(string eventBinderKey) 164 | { 165 | return Emitters[eventBinderKey]; 166 | } 167 | 168 | protected void SetEventEmitterErrorHandler(Action errorHandler) 169 | { 170 | foreach (IEventBinder binder in Emitters.Values) 171 | { 172 | binder.ErrorHandler = errorHandler; 173 | } 174 | } 175 | } 176 | } -------------------------------------------------------------------------------- /PusherClient/EventEmitterGeneric.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace PusherClient 5 | { 6 | public class EventEmitter : IEventEmitter 7 | { 8 | private readonly object _sync = new object(); 9 | private readonly IDictionary>> _listeners = new SortedList>>(); 10 | private readonly IList> _generalListeners = new List>(); 11 | 12 | /// 13 | /// Gets or sets the Pusher error handler. 14 | /// 15 | public Action ErrorHandler { get; set; } 16 | 17 | /// 18 | /// Gets whether any listeners are listening. 19 | /// 20 | public bool HasListeners 21 | { 22 | get 23 | { 24 | return _listeners.Count > 0 || _generalListeners.Count > 0; 25 | } 26 | } 27 | 28 | /// 29 | /// Binds to a given event with . 30 | /// 31 | /// The event name to listen for. 32 | /// The action to perform when the event occurs. 33 | public void Bind(string eventName, Action listener) 34 | { 35 | Guard.EventName(eventName); 36 | 37 | if (listener != null) 38 | { 39 | lock (_sync) 40 | { 41 | if (!_listeners.ContainsKey(eventName)) 42 | { 43 | _listeners[eventName] = new List>(); 44 | } 45 | 46 | if (!_listeners[eventName].Contains(listener)) 47 | { 48 | _listeners[eventName].Add(listener); 49 | } 50 | } 51 | } 52 | } 53 | 54 | /// 55 | /// Binds a listener that listens to all events. 56 | /// 57 | /// The action to perform when any event occurs. 58 | public void Bind(Action listener) 59 | { 60 | if (listener != null) 61 | { 62 | lock (_sync) 63 | { 64 | if (!_generalListeners.Contains(listener)) 65 | { 66 | _generalListeners.Add(listener); 67 | } 68 | } 69 | } 70 | } 71 | 72 | /// 73 | /// Remove the action for the event name. 74 | /// 75 | /// The name of the event to unbind. 76 | /// The action to remove. 77 | public void Unbind(string eventName, Action listener) 78 | { 79 | Guard.EventName(eventName); 80 | 81 | if (listener != null) 82 | { 83 | lock (_sync) 84 | { 85 | if (_listeners.ContainsKey(eventName)) 86 | { 87 | _listeners[eventName].Remove(listener); 88 | } 89 | } 90 | } 91 | } 92 | 93 | /// 94 | /// Removes the binding for the given event name 95 | /// 96 | /// The name of the event to unbind 97 | public void Unbind(string eventName) 98 | { 99 | Guard.EventName(eventName); 100 | 101 | lock (_sync) 102 | { 103 | if (_listeners.ContainsKey(eventName)) 104 | { 105 | _listeners[eventName].Clear(); 106 | _listeners.Remove(eventName); 107 | } 108 | } 109 | } 110 | 111 | /// 112 | /// Unbinds a listener that listens to all events. 113 | /// 114 | /// The listener to unbind. 115 | public void Unbind(Action listener) 116 | { 117 | if (listener != null) 118 | { 119 | lock (_sync) 120 | { 121 | _generalListeners.Remove(listener); 122 | } 123 | } 124 | } 125 | 126 | /// 127 | /// Removes all bindings. 128 | /// 129 | public void UnbindAll() 130 | { 131 | lock (_sync) 132 | { 133 | _generalListeners.Clear(); 134 | foreach (var list in _listeners.Values) 135 | { 136 | list.Clear(); 137 | } 138 | 139 | _listeners.Clear(); 140 | } 141 | } 142 | 143 | /// 144 | /// Emits an event. 145 | /// 146 | /// The name of the event to emit. 147 | /// The event data. 148 | public void EmitEvent(string eventName, TData data) 149 | { 150 | if (HasListeners) 151 | { 152 | List> generalListeners = new List>(); 153 | List> listeners = new List>(); 154 | lock (_sync) 155 | { 156 | 157 | if (_generalListeners.Count > 0) 158 | { 159 | foreach (var action in _generalListeners) 160 | { 161 | generalListeners.Add(action); 162 | } 163 | } 164 | 165 | if (_listeners.Count > 0) 166 | { 167 | if (_listeners.ContainsKey(eventName)) 168 | { 169 | listeners.AddRange(_listeners[eventName]); 170 | } 171 | } 172 | } 173 | 174 | foreach (var action in generalListeners) 175 | { 176 | try 177 | { 178 | action(eventName, data); 179 | } 180 | catch (Exception e) 181 | { 182 | HandleException(e, eventName, data); 183 | } 184 | } 185 | 186 | foreach (var action in listeners) 187 | { 188 | try 189 | { 190 | action(data); 191 | } 192 | catch (Exception e) 193 | { 194 | HandleException(e, eventName, data); 195 | } 196 | } 197 | } 198 | } 199 | 200 | private void HandleException(Exception e, string eventName, TData data) 201 | { 202 | if (ErrorHandler != null) 203 | { 204 | PusherException errorToHandle = e as PusherException; 205 | if (errorToHandle == null) 206 | { 207 | errorToHandle = new EventEmitterActionException(ErrorCodes.EventEmitterActionError, eventName, data, e); 208 | } 209 | 210 | ErrorHandler.Invoke(errorToHandle); 211 | } 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /PusherClient.Tests/UnitTests/PusherTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using NUnit.Framework; 4 | using PusherClient.Tests.Utilities; 5 | 6 | namespace PusherClient.Tests.UnitTests 7 | { 8 | [TestFixture] 9 | public class PusherTest 10 | { 11 | [Test] 12 | public void PusherShouldThrowAnExceptionWhenInitialisedWithANullApplicationKey() 13 | { 14 | // Arrange 15 | var stubOptions = new PusherOptions(); 16 | 17 | ArgumentException caughtException = null; 18 | 19 | // Act 20 | try 21 | { 22 | new Pusher(null, stubOptions); 23 | } 24 | catch (ArgumentException ex) 25 | { 26 | caughtException = ex; 27 | } 28 | 29 | // Assert 30 | Assert.IsNotNull(caughtException); 31 | StringAssert.Contains("The application key cannot be null or whitespace", caughtException.Message); 32 | } 33 | 34 | [Test] 35 | public void PusherShouldThrowAnExceptionWhenInitialisedWithAnEmptyApplicationKey() 36 | { 37 | // Arrange 38 | var stubOptions = new PusherOptions(); 39 | 40 | ArgumentException caughtException = null; 41 | 42 | // Act 43 | try 44 | { 45 | new Pusher(string.Empty, stubOptions); 46 | } 47 | catch (ArgumentException ex) 48 | { 49 | caughtException = ex; 50 | } 51 | 52 | // Assert 53 | Assert.IsNotNull(caughtException); 54 | StringAssert.Contains("The application key cannot be null or whitespace", caughtException.Message); 55 | } 56 | 57 | [Test] 58 | public void PusherShouldThrowAnExceptionWhenInitialisedWithAWhitespaceApplicationKey() 59 | { 60 | // Arrange 61 | var stubOptions = new PusherOptions(); 62 | 63 | ArgumentException caughtException = null; 64 | 65 | // Act 66 | try 67 | { 68 | new Pusher(" ", stubOptions); 69 | } 70 | catch (ArgumentException ex) 71 | { 72 | caughtException = ex; 73 | } 74 | 75 | // Assert 76 | Assert.IsNotNull(caughtException); 77 | StringAssert.Contains("The application key cannot be null or whitespace", caughtException.Message); 78 | } 79 | 80 | [Test] 81 | public void PusherShouldUseAPassedInPusherOptionsWhenSupplied() 82 | { 83 | // Arrange 84 | var mockOptions = new PusherOptions() 85 | { 86 | Cluster = "MyCluster" 87 | }; 88 | 89 | // Act 90 | var pusher = new Pusher("FakeKey", mockOptions); 91 | 92 | //Assert 93 | Assert.AreEqual(mockOptions, pusher.Options); 94 | } 95 | 96 | [Test] 97 | public void PusherShouldCreateANewPusherOptionsWhenNoPusherOptionsAreSupplied() 98 | { 99 | // Arrange 100 | 101 | // Act 102 | var pusher = new Pusher("FakeKey"); 103 | 104 | //Assert 105 | Assert.IsNotNull(pusher.Options); 106 | } 107 | 108 | [Test] 109 | public async Task PusherShouldThrowAnExceptionWhenSubscribeIsCalledWithAnEmptyStringForAChannelNameAsync() 110 | { 111 | // Arrange 112 | ArgumentException caughtException = null; 113 | 114 | // Act 115 | try 116 | { 117 | var pusher = new Pusher("FakeAppKey"); 118 | var channel = await pusher.SubscribeAsync(string.Empty).ConfigureAwait(false); 119 | } 120 | catch (ArgumentException ex) 121 | { 122 | caughtException = ex; 123 | } 124 | 125 | // Assert 126 | Assert.IsNotNull(caughtException); 127 | StringAssert.Contains("Value cannot be null.", caughtException.Message); 128 | } 129 | 130 | [Test] 131 | public async Task PusherShouldThrowAnExceptionWhenSubscribeIsCalledWithANullStringForAChannelNameAsync() 132 | { 133 | // Arrange 134 | ArgumentException caughtException = null; 135 | 136 | // Act 137 | try 138 | { 139 | var pusher = new Pusher("FakeAppKey"); 140 | var channel = await pusher.SubscribeAsync(null).ConfigureAwait(false); 141 | } 142 | catch (ArgumentException ex) 143 | { 144 | caughtException = ex; 145 | } 146 | 147 | // Assert 148 | Assert.IsNotNull(caughtException); 149 | StringAssert.Contains("Value cannot be null.", caughtException.Message); 150 | } 151 | 152 | [Test] 153 | public async Task PusherShouldThrowAnExceptionWhenSubscribePresenceIsCalledWithANonPresenceChannelAsync() 154 | { 155 | // Arrange 156 | ArgumentException caughtException = null; 157 | string channelName = "private-123"; 158 | 159 | // Act 160 | try 161 | { 162 | var pusher = new Pusher("FakeAppKey"); 163 | var channel = await pusher.SubscribePresenceAsync(channelName).ConfigureAwait(false); 164 | } 165 | catch (ArgumentException ex) 166 | { 167 | caughtException = ex; 168 | } 169 | 170 | // Assert 171 | Assert.IsNotNull(caughtException); 172 | StringAssert.Contains($"The channel name '{channelName}' is not that of a presence channel.", caughtException.Message); 173 | } 174 | 175 | [Test] 176 | public async Task PusherShouldThrowAnExceptionWhenSubscribePresenceIsCalledWithADifferentTypeAsync() 177 | { 178 | // Arrange 179 | ChannelException caughtException = null; 180 | 181 | // Act 182 | var pusher = new Pusher("FakeAppKey", new PusherOptions { Authorizer = new FakeAuthoriser("test") }); 183 | await pusher.SubscribePresenceAsync("presence-123").ConfigureAwait(false); 184 | 185 | try 186 | { 187 | await pusher.SubscribePresenceAsync("presence-123").ConfigureAwait(false); 188 | } 189 | catch (ChannelException ex) 190 | { 191 | caughtException = ex; 192 | } 193 | 194 | // Assert 195 | Assert.IsNotNull(caughtException); 196 | StringAssert.Contains("The presence channel 'presence-123' has already been created but with a different type", caughtException.Message); 197 | } 198 | 199 | [Test] 200 | public async Task PusherShouldThrowAnExceptionWhenSubscribePresenceIsCalledAfterSubscribeAsync() 201 | { 202 | // Arrange 203 | ChannelException caughtException = null; 204 | 205 | // Act 206 | var pusher = new Pusher("FakeAppKey", new PusherOptions { Authorizer = new FakeAuthoriser("test") }); 207 | await pusher.SubscribeAsync("presence-123").ConfigureAwait(false); 208 | 209 | try 210 | { 211 | await pusher.SubscribePresenceAsync("presence-123").ConfigureAwait(false); 212 | } 213 | catch (ChannelException ex) 214 | { 215 | caughtException = ex; 216 | } 217 | 218 | // Assert 219 | Assert.IsNotNull(caughtException); 220 | StringAssert.Contains("The presence channel 'presence-123' has already been created as a PresenceChannel", caughtException.Message); 221 | } 222 | } 223 | } --------------------------------------------------------------------------------