├── .gitattributes ├── .github ├── stale.yml └── workflows │ ├── build.yml │ ├── gh-release.yml │ ├── publish.yml │ ├── release.yml │ └── sign.yml ├── .gitignore ├── AppConfig.sample.json ├── CHANGELOG.md ├── LICENSE.txt ├── NetFxOnLinux.props ├── PusherServer.Tests ├── AcceptanceTests │ ├── AuthenticateUser.cs │ ├── AuthorizeChannel.cs │ ├── ChannelState.cs │ ├── Get.cs │ ├── PresenceChannels.cs │ ├── Trigger.cs │ └── percent-message.json ├── Config.cs ├── GlobalSuppressions.cs ├── Helpers │ ├── ApplicationConfig.cs │ ├── ClientServerFactory.cs │ ├── DataHelper.cs │ ├── EnvironmentVariableConfigLoader.cs │ ├── ExpectedExceptionAttribute.cs │ ├── IApplicationConfig.cs │ ├── IApplicationConfigLoader.cs │ ├── InMemoryChannelAuthorizer.cs │ ├── InMemoryUserAuthenticator.cs │ └── JsonFileConfigLoader.cs ├── PusherServer.Tests.csproj ├── RestfulClient │ ├── AuthenticationRequestFactoryTests.cs │ ├── Fakes │ │ ├── MyTestObject.cs │ │ ├── TestObjectFactory.cs │ │ └── TestOccupied.cs │ ├── PusherRestClientTests.cs │ └── PusherRestRequestTests.cs └── UnitTests │ ├── Authenticate.cs │ ├── AuthenticateUser.cs │ ├── AuthorizeChannel.cs │ ├── ChannelsList.cs │ ├── Get.cs │ ├── GetResult.cs │ ├── Pusher.cs │ ├── PusherOptions.cs │ ├── RawBodySerializer.cs │ ├── Trigger.cs │ ├── TriggerResult.cs │ └── WebHook.cs ├── PusherServer.public.snk ├── PusherServer ├── BatchEvent.cs ├── BatchTriggerBody.cs ├── ChannelAttributes.cs ├── ChannelAuthorizationResponse.cs ├── ChannelDataEncrypter.cs ├── ChannelsList.cs ├── CryptoHelper.cs ├── DefaultDeserializer.cs ├── DefaultSerializer.cs ├── EncryptedChannelData.cs ├── Event.cs ├── EventIdData.cs ├── Exceptions │ ├── ChannelNameFormatException.cs │ ├── ChannelNameLengthExceededException.cs │ ├── EncryptionMasterKeyException.cs │ ├── EventBatchSizeExceededException.cs │ ├── EventDataSizeExceededException.cs │ ├── SocketIdFormatException.cs │ └── TriggerResponseException.cs ├── GetResult.cs ├── IAuthenticationData.cs ├── IChannelAuthorizationResponse.cs ├── IChannelDataEncrypter.cs ├── IDeserializeJsonStrings.cs ├── IGetResult.cs ├── IPusher.cs ├── IPusherOptions.cs ├── IRequestResult.cs ├── ISerializeObjectsToJson.cs ├── ITriggerOptions.cs ├── ITriggerResult.cs ├── IUserAuthenticationResponse.cs ├── IWebHook.cs ├── PresenceChannelData.cs ├── Properties │ ├── AssemblyInfo.Signed.cs │ ├── AssemblyInfo.cs │ └── icon-128.png ├── Pusher.cs ├── PusherOptions.cs ├── PusherServer.csproj ├── RawBodySerializer.cs ├── RequestResult.cs ├── RestfulClient │ ├── AuthenticatedRequestFactory.cs │ ├── IAuthenticatedRequestFactory.cs │ ├── IPusherRestClient.cs │ ├── IPusherRestRequest.cs │ ├── PusherMethod.cs │ ├── PusherRestClient.cs │ └── PusherRestRequest.cs ├── TriggerBody.cs ├── TriggerOptions.cs ├── TriggerResult.cs ├── UserAuthenticationResponse.cs ├── UserData.cs ├── Util │ ├── DebugTraceLogger.cs │ ├── ITraceLogger.cs │ └── ReadOnlyDictionary.cs ├── ValidationHelper.cs ├── WebHook.cs ├── WebHookData.cs └── WebHookEvent.cs ├── README.md ├── Root.Build.props ├── StrongName ├── GeneratePusherKey.ps1 ├── GenerateStrongNameKey.cmd └── WritePusherKey.ps1 ├── build.sh ├── pull_request_template.md └── pusher-dotnet-server.sln /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | *.sh eol=lf 6 | 7 | ############################################################################### 8 | # Set default behavior for command prompt diff. 9 | # 10 | # This is need for earlier builds of msysgit that does not have it on by 11 | # default for csharp files. 12 | # Note: This is only used by command line 13 | ############################################################################### 14 | #*.cs diff=csharp 15 | 16 | ############################################################################### 17 | # Set the merge driver for project and solution files 18 | # 19 | # Merging from the command prompt will add diff markers to the files if there 20 | # are conflicts (Merging from VS is not affected by the settings below, in VS 21 | # the diff markers are never inserted). Diff markers may cause the following 22 | # file extensions to fail to load in VS. An alternative would be to treat 23 | # these files as binary and thus will always conflict and require user 24 | # intervention with every merge. To do so, just uncomment the entries below 25 | ############################################################################### 26 | #*.sln merge=binary 27 | #*.csproj merge=binary 28 | #*.vbproj merge=binary 29 | #*.vcxproj merge=binary 30 | #*.vcproj merge=binary 31 | #*.dbproj merge=binary 32 | #*.fsproj merge=binary 33 | #*.lsproj merge=binary 34 | #*.wixproj merge=binary 35 | #*.modelproj merge=binary 36 | #*.sqlproj merge=binary 37 | #*.wwaproj merge=binary 38 | 39 | ############################################################################### 40 | # behavior for image files 41 | # 42 | # image files are treated as binary by default. 43 | ############################################################################### 44 | #*.jpg binary 45 | #*.png binary 46 | #*.gif binary 47 | 48 | ############################################################################### 49 | # diff behavior for common document formats 50 | # 51 | # Convert binary document formats to text before diffing them. This feature 52 | # is only available from the command line. Turn it on by uncommenting the 53 | # entries below. 54 | ############################################################################### 55 | #*.doc diff=astextplain 56 | #*.DOC diff=astextplain 57 | #*.docx diff=astextplain 58 | #*.DOCX diff=astextplain 59 | #*.dot diff=astextplain 60 | #*.DOT diff=astextplain 61 | #*.pdf diff=astextplain 62 | #*.PDF diff=astextplain 63 | #*.rtf diff=astextplain 64 | #*.RTF diff=astextplain -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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-2019 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-server.sln 21 | - name: Build 22 | run: msbuild /p:deterministic=true /p:msbuildArchitecture=x64 /p:configuration=Release pusher-dotnet-server.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 "./PusherServer.Tests/bin/Release/net45/PusherServer.Tests.dll" /TestAdapterPath:"./PusherServer.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-server.sln 37 | - name: Test pack with strong named assembly 38 | run: msbuild /t:Pack /p:SignAssembly=true /p:configuration=release PusherServer/PusherServer.csproj 39 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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-2019 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-server.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-server.sln 25 | - name: Pack 26 | run: msbuild /t:Pack /p:SignAssembly=true /p:configuration=release PusherServer/PusherServer.csproj 27 | - name: Publish 28 | run: nuget push PusherServer\bin\release\PusherServer.*.nupkg -NonInteractive -Source https://api.nuget.org/v3/index.json -SkipDuplicate -ApiKey ${{ secrets.NUGET_API_KEY }} 29 | -------------------------------------------------------------------------------- /.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 PusherServer | grep PusherServer | 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 | env: 60 | PULL_REQUEST_BODY: ${{ github.event.pull_request.body }} 61 | run: | 62 | echo "$PULL_REQUEST_BODY" | csplit -s - "/##/" 63 | echo "# Changelog 64 | 65 | ## ${{ env.VERSION }}" >> CHANGELOG.tmp 66 | grep "^*" xx01 >> CHANGELOG.tmp 67 | grep -v "^# " CHANGELOG.md >> CHANGELOG.tmp 68 | cp CHANGELOG.tmp CHANGELOG.md 69 | git add Root.Build.props CHANGELOG.md 70 | git commit -m "Bump to version ${{ env.VERSION }}" 71 | - name: Push 72 | run: git push 73 | 74 | -------------------------------------------------------------------------------- /.github/workflows/sign.yml: -------------------------------------------------------------------------------- 1 | name: Test signing 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [ master, develop ] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: windows-2019 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Setup MS Build 16 | uses: microsoft/setup-msbuild@v1.0.2 17 | - name: Write code signing key 18 | env: 19 | CI_CODE_SIGN_KEY: ${{ secrets.CI_CODE_SIGN_KEY }} 20 | run: | 21 | ./StrongName/WritePusherKey.ps1 22 | - name: Restore dependencies 23 | run: nuget restore pusher-dotnet-server.sln 24 | - name: Build 25 | run: msbuild /p:SignAssembly=true /p:deterministic=true /p:msbuildArchitecture=x64 /p:configuration=Release pusher-dotnet-server.sln 26 | - name: Pack 27 | run: msbuild /t:Pack /p:SignAssembly=true /p:configuration=release PusherServer/PusherServer.csproj 28 | 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _ReSharper.*/ 2 | _UpgradeReport_Files 3 | UpgradeLog.XML 4 | Backup 5 | Download 6 | [Bb]in 7 | [Oo]bj 8 | *.suo 9 | *.user 10 | PusherRESTDotNet/PusherRESTDotNet.1.0.nupkg 11 | packages/ 12 | PusherRESTDotNet.sln.docstates 13 | build 14 | *.testsettings 15 | *.vsmdi 16 | msbuild.log 17 | *.nupkg 18 | .vs/ 19 | PusherServer.Core/project.lock.json 20 | *.userprefs 21 | TestResults/ 22 | AppConfig.test.json 23 | PusherServer.snk -------------------------------------------------------------------------------- /AppConfig.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppId": "", 3 | "AppKey": "", 4 | "AppSecret": "", 5 | "Cluster": "mt1" 6 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 5.0.0 4 | * [ADDED] Add support for channel attributes when triggering events 5 | 6 | ## 4.7.0-beta.2 7 | * [ADDED] Constructor for PusherRestClient accepting an externally supplied HttpClient 8 | 9 | ## 4.7.0-beta 10 | * [ADDED] AuthenticateUser 11 | * [ADDED] AuthorizeChannel 12 | * [DEPRECATED] Authenticate 13 | * [CHANGED] Bump Newtonsoft.Json to version 13.0.2 14 | 15 | ## 4.6.1 16 | * [FIXED] PusherClient test integration to 2.1.0. 17 | 18 | ## 4.6.0 19 | * [ADDED] Strong name to the PusherServer assembly. 20 | 21 | ## 4.5.0 22 | * [ADDED] End-to-end encryption using NaCl.Net. 23 | * [ADDED] EncryptionMasterKey and RestClientTimeout properties to IPusherOptions. 24 | * [ADDED] Cluster property to test application settings. 25 | * [CHANGED] Default timeout from 100 to 30 seconds. 26 | 27 | ## 4.4.0 28 | * [REMOVED] PusherServer.Core project 29 | * [FIXED] Use .ConfigureAwait(false) on every await 30 | 31 | ## 4.3.2 32 | * [CHANGED] Make the GH release task depend tag creation 33 | 34 | ## 4.3.1 35 | * [CHANGED] PusherServer.Core should not be packed 36 | 37 | ## 4.3.0 38 | * [CHANGED] PusherServer and PusherServer.Core project structure to target net45, net472, netstandard1.3 and netstandard2.0 39 | * [FIXED] TriggerResult exception text should include content 40 | * [CHANGED] Bump NUnit version to 3.11 41 | * [FIXED] Failing tests for Pusher apps in a cluster other than the default 42 | * [ADDED] Json config file for test application settings 43 | * [ADDED] Exception classes EventBatchSizeExceededException and EventDataSizeExceededException for client side validation 44 | * [ADDED] BatchEventDataSizeLimit to IPusherOptions 45 | * [CHANGED] Default Json serializer to use the option NullValueHandling.Ignore 46 | * [ADDED] Exception classes ChannelNameFormatException, ChannelNameLengthExceededException and SocketIdFormatException 47 | * [ADDED] Interface ITraceLogger and applied it as a property to IPusherOptions 48 | 49 | ## 4.2.0 50 | * [CHANGED] Project now targets DotNet Standard 1.6. The API has not changed. 51 | 52 | ## 4.0.0-rc1, 4.1.0 53 | * [ADDED] support for the .NET Core 54 | * [CHANGED] The library now is built against .NET 4.5 55 | * [CHANGED] Removed the dependency on RestSharp, and replaced with the .NETs HttpClient class. 56 | * [REMOVED] Retired the callback syntax 57 | * [ADDED] Async/Await based syntax 58 | * [FIXED] The Out of range exceptions thrown by Trigger methods to correctly specify the message, instead of the message be supplied as the parameter name 59 | 60 | ## 3.0.0 rc4 61 | 62 | * [ADDED] Support for the `cluster` option. 63 | 64 | ## 3.0.0 rc3 65 | 66 | * [ADDED] Build support for the Pusher Travis instance 67 | * [CHANGED] Trigger based calls that result in a bad response no longer throw TriggerResponseExceptions. The response and original content are now available as properties for interrogation 68 | * [ADDED] The Pusher HTTP API Host & Port can now be set via PusherOptions.HostName & PusherOptions.Port properties 69 | * [ADDED] TriggerAsync to allow asynchronous requests to be made to the HTTP API. 70 | * [ADDED] New API abstractions onto Pusher for fetching info on single and multiple channels, and fetching users from a presence Channel. Both sync and sync calls are supported. 71 | * [REMOVED] Event Buffer support 72 | * [ADDED] Support for providing a different JSON serializer & deserializer 73 | 74 | ## 3.0.0 rc1/2 75 | 76 | * [CHANGED] `Trigger` calls that result in a non 200 response from the Pusher HTTP API now result in a `TriggerResponseException` being thrown. 77 | This is a BREAKING CHANGE as previously you could inspect the `ITriggerResult.StatusCode` to detect a failed request. 78 | * [ADDED] When triggering events against a Pusher cluster that supports Event Buffer functionality the IDs of the triggered events 79 | can be retrieved via `ITriggerResult.EventIds` 80 | * [ADDED] The Pusher HTTP API Host can now be set via `PusherOptions.Host` 81 | * [ADDED] `TriggerAsync` to allow asynchronous requests to be made to the HTTP API. 82 | 83 | ## 2.1.1 84 | 85 | * [FIXED] Channel name and socket_id values are validated for `Pusher.Trigger` 86 | * [FIXED] socket_id values are validated for `Pusher.Authenticate` 87 | 88 | ## 2.1.0 89 | 90 | * [FIXED] Pusher.Authenticate for private channels. Should not return `channel_data` in the JSON. 91 | * [CHANGED] IAuthenticationData Pusher.Authenticate(string channelName, string socketId, PresenceChannelData presenceData) to throw a ArgumentNullException if `presenceData` is `null`. 92 | 93 | ## 2.0.0 94 | 95 | * Full release of 2.0.0 library 96 | * Added `Pusher.ProcessWebHook(signature, body)` support 97 | 98 | ## 2.0.0-beta-5 99 | 100 | * Updating RestSharp to 105.0.1 101 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 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 | -------------------------------------------------------------------------------- /NetFxOnLinux.props: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | true 8 | 9 | 10 | /Library/Frameworks/Mono.framework/Versions/Current/lib/mono 11 | /usr/lib/mono 12 | /usr/local/lib/mono 13 | 14 | 15 | $(BaseFrameworkPathOverrideForMono)/4.5-api 16 | $(BaseFrameworkPathOverrideForMono)/4.5.1-api 17 | $(BaseFrameworkPathOverrideForMono)/4.5.2-api 18 | $(BaseFrameworkPathOverrideForMono)/4.6-api 19 | $(BaseFrameworkPathOverrideForMono)/4.6.1-api 20 | $(BaseFrameworkPathOverrideForMono)/4.6.2-api 21 | $(BaseFrameworkPathOverrideForMono)/4.7-api 22 | $(BaseFrameworkPathOverrideForMono)/4.7.1-api 23 | $(BaseFrameworkPathOverrideForMono)/4.7.2-api 24 | true 25 | 26 | 27 | $(FrameworkPathOverride)/Facades;$(AssemblySearchPaths) 28 | 29 | -------------------------------------------------------------------------------- /PusherServer.Tests/AcceptanceTests/AuthenticateUser.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using NUnit.Framework; 3 | using PusherServer.Tests.Helpers; 4 | 5 | namespace PusherServer.Tests.AcceptanceTests 6 | { 7 | [TestFixture] 8 | public class When_authenticating_a_user 9 | { 10 | [Test] 11 | public async Task the_auth_token_for_a_user_should_be_accepted_by_Pusher() 12 | { 13 | PusherServer.Pusher pusherServer = new Pusher(Config.AppId, Config.AppKey, Config.AppSecret, new PusherOptions() 14 | { 15 | HostName = Config.HttpHost 16 | }); 17 | PusherClient.Pusher pusherClient = new PusherClient.Pusher(Config.AppKey, new PusherClient.PusherOptions 18 | { 19 | UserAuthenticator = new InMemoryUserAuthenticator( 20 | pusherServer, 21 | new UserData() 22 | { 23 | id = "leggetter", 24 | watchlist = new string[] { "user_1", "user_2" }, 25 | user_info = new { twitter_id = "@leggetter" } 26 | }), 27 | Cluster = Config.Cluster, 28 | TraceLogger = new PusherClient.TraceLogger(), 29 | }); 30 | 31 | await pusherClient.ConnectAsync().ConfigureAwait(false); 32 | pusherClient.User.Signin(); 33 | 34 | await pusherClient.User.SigninDoneAsync().ConfigureAwait(false); 35 | 36 | // No assertions for now. If the above code executes without error then the test passes. 37 | } 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /PusherServer.Tests/AcceptanceTests/AuthorizeChannel.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using NUnit.Framework; 3 | using PusherServer.Tests.Helpers; 4 | 5 | namespace PusherServer.Tests.AcceptanceTests 6 | { 7 | [TestFixture] 8 | public class When_authorizing_a_private_subscription 9 | { 10 | [Test] 11 | public async Task the_auth_token_for_a_private_channel_should_be_accepted_by_Pusher() 12 | { 13 | PusherServer.Pusher pusherServer = new Pusher(Config.AppId, Config.AppKey, Config.AppSecret, new PusherOptions() 14 | { 15 | HostName = Config.HttpHost 16 | }); 17 | PusherClient.Pusher pusherClient = new PusherClient.Pusher(Config.AppKey, new PusherClient.PusherOptions 18 | { 19 | ChannelAuthorizer = new InMemoryChannelAuthorizer(pusherServer), 20 | Cluster = Config.Cluster, 21 | TraceLogger = new PusherClient.TraceLogger(), 22 | }); 23 | 24 | string channelName = "private-channel"; 25 | 26 | await pusherClient.ConnectAsync().ConfigureAwait(false); 27 | 28 | var channel = await pusherClient.SubscribeAsync(channelName).ConfigureAwait(false); 29 | 30 | Assert.IsTrue(channel.IsSubscribed, nameof(channel.IsSubscribed)); 31 | } 32 | 33 | [Test] 34 | public async Task the_auth_token_for_a_private_encrypted_channel_should_be_accepted_by_Pusher() 35 | { 36 | PusherServer.Pusher pusherServer = new Pusher(Config.AppId, Config.AppKey, Config.AppSecret, new PusherOptions() 37 | { 38 | HostName = Config.HttpHost, 39 | EncryptionMasterKey = DataHelper.GenerateEncryptionMasterKey(), 40 | }); 41 | 42 | PusherClient.Pusher pusherClient = new PusherClient.Pusher(Config.AppKey, new PusherClient.PusherOptions 43 | { 44 | ChannelAuthorizer = new InMemoryChannelAuthorizer(pusherServer), 45 | Cluster = Config.Cluster, 46 | TraceLogger = new PusherClient.TraceLogger(), 47 | }); 48 | 49 | string channelName = "private-encrypted-channel"; 50 | 51 | await pusherClient.ConnectAsync().ConfigureAwait(false); 52 | 53 | var channel = await pusherClient.SubscribeAsync(channelName).ConfigureAwait(false); 54 | 55 | Assert.IsTrue(channel.IsSubscribed, nameof(channel.IsSubscribed)); 56 | } 57 | 58 | [Test] 59 | public async Task the_auth_token_for_a_presence_channel_should_be_accepted_by_Pusher() 60 | { 61 | Pusher pusherServer = new Pusher(Config.AppId, Config.AppKey, Config.AppSecret, new PusherOptions 62 | { 63 | HostName = Config.HttpHost, 64 | }); 65 | PusherClient.Pusher pusherClient = new PusherClient.Pusher(Config.AppKey, new PusherClient.PusherOptions 66 | { 67 | ChannelAuthorizer = new InMemoryChannelAuthorizer( 68 | pusherServer, 69 | new PresenceChannelData() 70 | { 71 | user_id = "leggetter", 72 | user_info = new { twitter_id = "@leggetter" } 73 | }), 74 | Cluster = Config.Cluster, 75 | TraceLogger = new PusherClient.TraceLogger(), 76 | }); 77 | 78 | string channelName = "presence-channel"; 79 | 80 | await pusherClient.ConnectAsync().ConfigureAwait(false); 81 | 82 | var channel = await pusherClient.SubscribePresenceAsync(channelName).ConfigureAwait(false); 83 | 84 | Assert.IsTrue(channel.IsSubscribed, nameof(channel.IsSubscribed)); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /PusherServer.Tests/AcceptanceTests/ChannelState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | using NUnit.Framework; 5 | using PusherServer.Tests.Helpers; 6 | 7 | namespace PusherServer.Tests.AcceptanceTests 8 | { 9 | [TestFixture] 10 | public class When_querying_a_channel 11 | { 12 | [Test] 13 | public async Task It_should_return_the_state_asynchronously_when_given_a_channel_name_that_exists() 14 | { 15 | var channelName = "presence-state-channel-async-1"; 16 | 17 | var pusherServer = ClientServerFactory.CreateServer(); 18 | await ClientServerFactory.CreateClientAsync(pusherServer, channelName).ConfigureAwait(false); 19 | 20 | var info = new { info = "user_count" }; 21 | var response = await pusherServer.FetchStateForChannelAsync(channelName, info).ConfigureAwait(false); 22 | 23 | Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); 24 | Assert.AreEqual(1, response.Data.User_Count); 25 | } 26 | 27 | [Test] 28 | public async Task It_should_return_the_state_asynchronously_when_given_a_channel_name_that_exists_and_no_info_object_is_provided() 29 | { 30 | var channelName = "presence-state-channel-async-1"; 31 | 32 | var pusherServer = ClientServerFactory.CreateServer(); 33 | await ClientServerFactory.CreateClientAsync(pusherServer, channelName).ConfigureAwait(false); 34 | 35 | var response = await pusherServer.FetchStateForChannelAsync(channelName).ConfigureAwait(false); 36 | 37 | Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); 38 | Assert.IsTrue(response.Data.Occupied); 39 | } 40 | 41 | [Test] 42 | public async Task It_should_not_return_the_state_based_asynchronously_when_given_a_channel_name_that_exists_an_bad_attributes() 43 | { 44 | var channelName = "presence-state-channel-async-2"; 45 | 46 | var pusherServer = ClientServerFactory.CreateServer(); 47 | await ClientServerFactory.CreateClientAsync(pusherServer, channelName).ConfigureAwait(false); 48 | 49 | var info = new { info = "does-not-exist" }; 50 | 51 | var result = await pusherServer.FetchStateForChannelAsync(channelName, info).ConfigureAwait(false); 52 | Assert.AreEqual(HttpStatusCode.BadRequest, result.StatusCode); 53 | StringAssert.IsMatch("info should be a comma separated list of attributes", result.Body); 54 | } 55 | 56 | [Test] 57 | public async Task It_should_throw_an_exception_when_given_an_empty_string_as_a_channel_name_async() 58 | { 59 | var pusherServer = ClientServerFactory.CreateServer(); 60 | 61 | var info = new { info = "user_count" }; 62 | 63 | ArgumentException caughtException = null; 64 | 65 | try 66 | { 67 | var response = await pusherServer.FetchStateForChannelAsync(string.Empty, info).ConfigureAwait(false); 68 | } 69 | catch (ArgumentException ex) 70 | { 71 | caughtException = ex; 72 | } 73 | 74 | StringAssert.IsMatch("channelName cannot be null or empty", caughtException.Message); 75 | } 76 | 77 | [Test] 78 | public async Task It_should_throw_an_exception_when_given_a_null_as_a_channel_name_async() 79 | { 80 | var pusherServer = ClientServerFactory.CreateServer(); 81 | 82 | var info = new { info = "user_count" }; 83 | 84 | ArgumentException caughtException = null; 85 | 86 | try 87 | { 88 | var response = await pusherServer.FetchStateForChannelAsync(null, info).ConfigureAwait(false); 89 | } 90 | catch (ArgumentException ex) 91 | { 92 | caughtException = ex; 93 | } 94 | 95 | StringAssert.IsMatch("channelName cannot be null or empty", caughtException.Message); 96 | } 97 | 98 | private class ChannelStateMessage 99 | { 100 | public bool Occupied { get; set; } 101 | public int User_Count { get; set; } 102 | } 103 | } 104 | 105 | [TestFixture] 106 | public class When_querying_multiple_channels 107 | { 108 | [Test] 109 | public async Task It_should_return_the_state_asynchronously_when_given_a_channel_name_that_exists() 110 | { 111 | var channelName = "presence-multiple-state-channel-async-3"; 112 | 113 | var pusherServer = ClientServerFactory.CreateServer(); 114 | await ClientServerFactory.CreateClientAsync(pusherServer, channelName).ConfigureAwait(false); 115 | 116 | var info = new { info = "user_count", filter_by_prefix = "presence-" }; 117 | 118 | var result = await pusherServer.FetchStateForChannelsAsync(info).ConfigureAwait(false); 119 | 120 | Assert.AreEqual(HttpStatusCode.OK, result.StatusCode); 121 | Assert.AreEqual(1, ((Newtonsoft.Json.Linq.JValue)((result.Data as Newtonsoft.Json.Linq.JObject)["channels"]["presence-multiple-state-channel-async-3"]["user_count"])).Value); 122 | } 123 | 124 | [Test] 125 | public async Task It_should_return_the_state_asynchronously_when_given_a_channel_name_that_exists_and_no_info_object_is_provided() 126 | { 127 | var channelName = "presence-multiple-state-channel-async-3"; 128 | 129 | var pusherServer = ClientServerFactory.CreateServer(); 130 | await ClientServerFactory.CreateClientAsync(pusherServer, channelName).ConfigureAwait(false); 131 | 132 | var result = await pusherServer.FetchStateForChannelsAsync().ConfigureAwait(false); 133 | 134 | Assert.AreEqual(HttpStatusCode.OK, result.StatusCode); 135 | } 136 | 137 | [Test] 138 | public async Task It_should_not_return_the_state_asynchronously_based_when_given_a_channel_name_that_exists_an_bad_attributes() 139 | { 140 | string channelName = "presence-multiple-state-channel-async-4"; 141 | 142 | var pusherServer = ClientServerFactory.CreateServer(); 143 | await ClientServerFactory.CreateClientAsync(pusherServer, channelName).ConfigureAwait(false); 144 | 145 | var info = new { info = "does-not-exist" }; 146 | 147 | var result = await pusherServer.FetchStateForChannelsAsync(info).ConfigureAwait(false); 148 | 149 | Assert.AreEqual(HttpStatusCode.BadRequest, result.StatusCode); 150 | StringAssert.IsMatch("info should be a comma separated list of attributes", result.Body); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /PusherServer.Tests/AcceptanceTests/Get.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Threading.Tasks; 3 | using NUnit.Framework; 4 | 5 | namespace PusherServer.Tests.AcceptanceTests 6 | { 7 | [TestFixture] 8 | public class When_application_channels_are_queried 9 | { 10 | [Test] 11 | public async Task It_should_return_a_200_response() 12 | { 13 | IPusher pusher = new Pusher(Config.AppId, Config.AppKey, Config.AppSecret, new PusherOptions() 14 | { 15 | HostName = Config.HttpHost 16 | }); 17 | 18 | IGetResult result = await pusher.GetAsync("/channels").ConfigureAwait(false); 19 | 20 | Assert.AreEqual(HttpStatusCode.OK, result.StatusCode); 21 | } 22 | 23 | [Test] 24 | public async Task It_should_be_possible_to_deserialize_the_request_result_body_as_an_object() 25 | { 26 | IPusher pusher = new Pusher(Config.AppId, Config.AppKey, Config.AppSecret, new PusherOptions() 27 | { 28 | HostName = Config.HttpHost 29 | }); 30 | 31 | IGetResult result = await pusher.GetAsync("/channels").ConfigureAwait(false); 32 | 33 | Assert.NotNull(result.Data); 34 | } 35 | 36 | [Test] 37 | public async Task It_should_be_possible_to_deserialize_the_a_channels_result_body_as_an_ChannelsList() 38 | { 39 | IPusher pusher = new Pusher(Config.AppId, Config.AppKey, Config.AppSecret, new PusherOptions() 40 | { 41 | HostName = Config.HttpHost 42 | }); 43 | 44 | IGetResult result = await pusher.GetAsync("/channels").ConfigureAwait(false); 45 | 46 | Assert.IsTrue(result.Data.Channels.Count >= 0); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /PusherServer.Tests/AcceptanceTests/PresenceChannels.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | using NUnit.Framework; 5 | using PusherServer.Tests.Helpers; 6 | 7 | namespace PusherServer.Tests.AcceptanceTests 8 | { 9 | [TestFixture] 10 | public class When_querying_the_Presence_Channel 11 | { 12 | [Test] 13 | public async Task Should_get_a_list_of_subscribed_users_asynchronously_when_using_the_correct_channel_name_and_users_are_subscribed() 14 | { 15 | string channelName = "presence-test-channel-async-1"; 16 | 17 | var pusherServer = ClientServerFactory.CreateServer(); 18 | await ClientServerFactory.CreateClientAsync(pusherServer, channelName).ConfigureAwait(false); 19 | 20 | IGetResult result = await pusherServer.FetchUsersFromPresenceChannelAsync(channelName).ConfigureAwait(false); 21 | 22 | Assert.AreEqual(HttpStatusCode.OK, result.StatusCode); 23 | Assert.AreEqual(1, result.Data.Users.Length); 24 | Assert.AreEqual("Mr Pusher", result.Data.Users[0].Id); 25 | } 26 | 27 | [Test] 28 | public async Task Should_get_an_empty_list_of_subscribed_users_asynchronously_when_using_the_correct_channel_name_and_no_users_are_subscribed() 29 | { 30 | string channelName = "presence-test-channel-async-2"; 31 | 32 | var pusherServer = ClientServerFactory.CreateServer(); 33 | 34 | IGetResult result = await pusherServer.FetchUsersFromPresenceChannelAsync(channelName).ConfigureAwait(false); 35 | 36 | Assert.AreEqual(HttpStatusCode.OK, result.StatusCode); 37 | Assert.AreEqual(0, result.Data.Users.Length); 38 | } 39 | 40 | [Test] 41 | public async Task should_return_bad_request_asynchronously_using_an_incorrect_channel_name_and_users_are_subscribed() 42 | { 43 | string channelName = "presence-test-channel-async-3"; 44 | 45 | var pusherServer = ClientServerFactory.CreateServer(); 46 | await ClientServerFactory.CreateClientAsync(pusherServer, channelName).ConfigureAwait(false); 47 | 48 | IGetResult result = await pusherServer.FetchUsersFromPresenceChannelAsync("test-channel-async").ConfigureAwait(false); 49 | 50 | Assert.AreEqual(HttpStatusCode.BadRequest, result.StatusCode); 51 | } 52 | 53 | [Test] 54 | public async Task should_throw_an_exception_when_given_a_null_for_a_channel_name_async() 55 | { 56 | var pusherServer = ClientServerFactory.CreateServer(); 57 | 58 | ArgumentException caughtException = null; 59 | 60 | try 61 | { 62 | var response = await pusherServer.FetchUsersFromPresenceChannelAsync(null).ConfigureAwait(false); 63 | } 64 | catch (ArgumentException ex) 65 | { 66 | caughtException = ex; 67 | } 68 | 69 | Assert.IsNotNull(caughtException); 70 | StringAssert.IsMatch("channelName cannot be null or empty", caughtException.Message); 71 | } 72 | 73 | [Test] 74 | public async Task should_throw_an_exception_when_given_an_empty_string_for_a_channel_name_async() 75 | { 76 | var pusherServer = ClientServerFactory.CreateServer(); 77 | 78 | ArgumentException caughtException = null; 79 | 80 | try 81 | { 82 | var response = await pusherServer.FetchUsersFromPresenceChannelAsync(string.Empty).ConfigureAwait(false); 83 | } 84 | catch (ArgumentException ex) 85 | { 86 | caughtException = ex; 87 | } 88 | 89 | Assert.IsNotNull(caughtException); 90 | StringAssert.IsMatch("channelName cannot be null or empty", caughtException.Message); 91 | } 92 | 93 | private class PresenceChannelMessage 94 | { 95 | public PresenceChannelUser[] Users { get; set; } 96 | } 97 | 98 | private class PresenceChannelUser 99 | { 100 | public string Id { get; set; } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /PusherServer.Tests/AcceptanceTests/percent-message.json: -------------------------------------------------------------------------------- 1 | {"ta":{"kttg":[{"id":"1019","n":"Kicks at goal","d":"Convert 75% of kicks at goal","v":100.0,"tv":75.0,"miv":0,"mav":100},{"id":"1151","n":"Running Metres","d":"Run more than 4 metres per carry on average","v":2.9,"tv":4.0,"miv":0,"mav":10}]}} -------------------------------------------------------------------------------- /PusherServer.Tests/Config.cs: -------------------------------------------------------------------------------- 1 | using PusherServer.Tests.Helpers; 2 | 3 | namespace PusherServer.Tests 4 | { 5 | internal static class Config 6 | { 7 | static Config() 8 | { 9 | IApplicationConfig config = EnvironmentVariableConfigLoader.Default.Load(); 10 | if (string.IsNullOrWhiteSpace(config.AppKey)) 11 | { 12 | config = JsonFileConfigLoader.Default.Load(); 13 | } 14 | 15 | AppId = config.AppId; 16 | AppKey = config.AppKey; 17 | AppSecret = config.AppSecret; 18 | Cluster = config.Cluster; 19 | HttpHost = config.HttpHost; 20 | WebSocketHost = config.WebSocketHost; 21 | } 22 | 23 | public static string AppId { get; private set; } 24 | 25 | public static string AppKey { get; private set; } 26 | 27 | public static string AppSecret { get; private set; } 28 | 29 | public static string Cluster { get; private set; } 30 | 31 | public static string HttpHost { get; private set; } 32 | 33 | public static string WebSocketHost { get; private set; } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /PusherServer.Tests/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Far too many cases")] 9 | -------------------------------------------------------------------------------- /PusherServer.Tests/Helpers/ApplicationConfig.cs: -------------------------------------------------------------------------------- 1 | namespace PusherServer.Tests.Helpers 2 | { 3 | /// 4 | /// Contains test application configuration. 5 | /// 6 | public class ApplicationConfig : IApplicationConfig 7 | { 8 | 9 | /// 10 | public string AppId { get; set; } 11 | 12 | /// 13 | public string AppKey { get; set; } 14 | 15 | /// 16 | public string AppSecret { get; set; } 17 | 18 | /// 19 | public string Cluster { get; set; } = "mt1"; 20 | 21 | /// 22 | public string HttpHost 23 | { 24 | get 25 | { 26 | return $"api-{Cluster}.pusher.com"; 27 | } 28 | } 29 | 30 | /// 31 | public string WebSocketHost 32 | { 33 | get 34 | { 35 | return $"ws-{Cluster}.pusher.com"; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /PusherServer.Tests/Helpers/ClientServerFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace PusherServer.Tests.Helpers 6 | { 7 | internal sealed class ClientServerFactory 8 | { 9 | /// 10 | /// Create a Pusher Client, and subscribes a user 11 | /// 12 | /// Server to connect to 13 | /// The name of the channel to subscribe to 14 | /// An awaitable task 15 | public static async Task CreateClientAsync(Pusher pusherServer, string channelName) 16 | { 17 | PusherClient.Pusher pusherClient = new PusherClient.Pusher(Config.AppKey, new PusherClient.PusherOptions 18 | { 19 | ChannelAuthorizer = new InMemoryChannelAuthorizer( 20 | pusherServer, 21 | new PresenceChannelData() 22 | { 23 | user_id = "Mr Pusher", 24 | user_info = new { twitter_id = "@pusher" } 25 | }), 26 | Cluster = Config.Cluster, 27 | TraceLogger = new PusherClient.TraceLogger(), 28 | }); 29 | 30 | await pusherClient.ConnectAsync().ConfigureAwait(false); 31 | 32 | AutoResetEvent subscribedEvent = new AutoResetEvent(false); 33 | await pusherClient.SubscribeAsync(channelName, (sender) => { subscribedEvent.Set(); } ).ConfigureAwait(false); 34 | subscribedEvent.WaitOne(TimeSpan.FromSeconds(5)); 35 | } 36 | 37 | /// 38 | /// Create a Pusher Server instance 39 | /// 40 | /// 41 | public static Pusher CreateServer() 42 | { 43 | return new Pusher(Config.AppId, Config.AppKey, Config.AppSecret, new PusherOptions 44 | { 45 | HostName = Config.HttpHost 46 | }); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /PusherServer.Tests/Helpers/DataHelper.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System.Collections.Generic; 3 | using System.Security.Cryptography; 4 | 5 | namespace PusherServer.Tests.Helpers 6 | { 7 | internal static class DataHelper 8 | { 9 | internal static List CreateEvents(int numberOfEvents) 10 | { 11 | return CreateEvents(numberOfEvents, eventSizeInBytes: "{}".Length); 12 | } 13 | 14 | internal static List CreateEvents(int numberOfEvents, int eventSizeInBytes, string channelId = "testChannel", string eventId = "testEvent") 15 | { 16 | Assert.IsTrue(eventSizeInBytes >= 2, "The event size must be greater than or equal to 2."); 17 | 18 | List events = new List(numberOfEvents); 19 | 20 | // We need to account for braces {} around the text when serialized. 21 | string data = new string('Q', eventSizeInBytes - "{}".Length); 22 | for (int i = 0; i < numberOfEvents; i++) 23 | { 24 | events.Add(new Event { Channel = $"{channelId}{i}", EventName = $"{eventId}{i}", Data = data }); 25 | } 26 | 27 | return events; 28 | } 29 | 30 | internal static byte[] GenerateEncryptionMasterKey() 31 | { 32 | byte[] encryptionMasterKey = new byte[32]; 33 | using (RandomNumberGenerator random = RandomNumberGenerator.Create()) 34 | { 35 | random.GetBytes(encryptionMasterKey); 36 | } 37 | 38 | return encryptionMasterKey; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /PusherServer.Tests/Helpers/EnvironmentVariableConfigLoader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherServer.Tests.Helpers 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 | 15 | public static IApplicationConfigLoader Default { get; } = new EnvironmentVariableConfigLoader(); 16 | 17 | /// 18 | /// Loads configuration from system environment variables. 19 | /// 20 | /// An instance. 21 | public IApplicationConfig Load() 22 | { 23 | ApplicationConfig result = new ApplicationConfig 24 | { 25 | AppId = Environment.GetEnvironmentVariable(PUSHER_APP_ID), 26 | AppKey = Environment.GetEnvironmentVariable(PUSHER_APP_KEY), 27 | AppSecret = Environment.GetEnvironmentVariable(PUSHER_APP_SECRET), 28 | Cluster = Environment.GetEnvironmentVariable(PUSHER_APP_CLUSTER), 29 | }; 30 | return result; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /PusherServer.Tests/Helpers/ExpectedExceptionAttribute.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using NUnit.Framework.Interfaces; 3 | using NUnit.Framework.Internal; 4 | using NUnit.Framework.Internal.Commands; 5 | using System; 6 | 7 | namespace PusherServer.Tests.Helpers 8 | { 9 | /// 10 | /// A simple ExpectedExceptionAttribute (from https://github.com/nunit/nunit-csharp-samples/blob/master/ExpectedExceptionExample/ExpectedExceptionAttribute.cs) 11 | /// See https://github.com/nunit/nunit/issues/799#issuecomment-137893339 for example 12 | /// 13 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] 14 | public class ExpectedExceptionAttribute : NUnitAttribute, IWrapTestMethod 15 | { 16 | private readonly Type _expectedExceptionType; 17 | 18 | public ExpectedExceptionAttribute(Type type) 19 | { 20 | _expectedExceptionType = type; 21 | } 22 | 23 | public TestCommand Wrap(TestCommand command) 24 | { 25 | return new ExpectedExceptionCommand(command, _expectedExceptionType); 26 | } 27 | 28 | private class ExpectedExceptionCommand : DelegatingTestCommand 29 | { 30 | private readonly Type _expectedType; 31 | 32 | public ExpectedExceptionCommand(TestCommand innerCommand, Type expectedType) 33 | : base(innerCommand) 34 | { 35 | _expectedType = expectedType; 36 | } 37 | 38 | public override TestResult Execute(TestExecutionContext context) 39 | { 40 | Type caughtType = null; 41 | 42 | try 43 | { 44 | innerCommand.Execute(context); 45 | } 46 | catch (Exception ex) 47 | { 48 | if (ex is NUnitException) 49 | ex = ex.InnerException; 50 | caughtType = ex.GetType(); 51 | } 52 | 53 | if (caughtType == _expectedType) 54 | context.CurrentResult.SetResult(ResultState.Success); 55 | else if (caughtType != null) 56 | context.CurrentResult.SetResult(ResultState.Failure, 57 | string.Format("Expected {0} but got {1}", _expectedType.Name, caughtType.Name)); 58 | else 59 | context.CurrentResult.SetResult(ResultState.Failure, 60 | string.Format("Expected {0} but no exception was thrown", _expectedType.Name)); 61 | 62 | return context.CurrentResult; 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /PusherServer.Tests/Helpers/IApplicationConfig.cs: -------------------------------------------------------------------------------- 1 | namespace PusherServer.Tests.Helpers 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 the Pusher server API host name. For example, api-mt1.pusher.com. 30 | /// 31 | string HttpHost { get; } 32 | 33 | /// 34 | /// Gets the Pusher client API host name. For example, ws-mt1.pusher.com. 35 | /// 36 | string WebSocketHost { get; } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PusherServer.Tests/Helpers/IApplicationConfigLoader.cs: -------------------------------------------------------------------------------- 1 | namespace PusherServer.Tests.Helpers 2 | { 3 | /// 4 | /// Loads application configuration. 5 | /// 6 | public interface IApplicationConfigLoader 7 | { 8 | /// 9 | /// Loads application configuration. 10 | /// 11 | /// An instance. 12 | IApplicationConfig Load(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /PusherServer.Tests/Helpers/InMemoryChannelAuthorizer.cs: -------------------------------------------------------------------------------- 1 | using PusherClient; 2 | 3 | namespace PusherServer.Tests.Helpers 4 | { 5 | internal class InMemoryChannelAuthorizer: IChannelAuthorizer 6 | { 7 | private readonly PusherServer.Pusher _pusher; 8 | private readonly PresenceChannelData _presenceData; 9 | 10 | public InMemoryChannelAuthorizer(PusherServer.Pusher pusher): 11 | this(pusher, null) 12 | { 13 | } 14 | 15 | public InMemoryChannelAuthorizer(PusherServer.Pusher pusher, PresenceChannelData presenceData) 16 | { 17 | _pusher = pusher; 18 | _presenceData = presenceData; 19 | } 20 | 21 | public string Authorize(string channelName, string socketId) 22 | { 23 | IAuthenticationData auth; 24 | if (_presenceData != null) 25 | { 26 | auth = _pusher.Authenticate(channelName, socketId, _presenceData); 27 | } 28 | else 29 | { 30 | auth = _pusher.Authenticate(channelName, socketId); 31 | } 32 | return auth.ToJson(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /PusherServer.Tests/Helpers/InMemoryUserAuthenticator.cs: -------------------------------------------------------------------------------- 1 | using PusherClient; 2 | 3 | namespace PusherServer.Tests.Helpers 4 | { 5 | internal class InMemoryUserAuthenticator: IUserAuthenticator 6 | { 7 | private readonly PusherServer.Pusher _pusher; 8 | private readonly UserData _userData; 9 | 10 | public InMemoryUserAuthenticator(PusherServer.Pusher pusher, UserData userData) 11 | { 12 | _pusher = pusher; 13 | _userData = userData; 14 | } 15 | 16 | public string Authenticate(string socketId) 17 | { 18 | IUserAuthenticationResponse authResponse; 19 | authResponse = _pusher.AuthenticateUser(socketId, _userData); 20 | return authResponse.ToJson(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /PusherServer.Tests/Helpers/JsonFileConfigLoader.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Reflection; 3 | using Newtonsoft.Json; 4 | 5 | namespace PusherServer.Tests.Helpers 6 | { 7 | /// 8 | /// Loads test configuration from a json file. 9 | /// 10 | public class JsonFileConfigLoader : IApplicationConfigLoader 11 | { 12 | private const string DefaultFileName = "AppConfig.test.json"; 13 | 14 | /// 15 | /// Creates an instance of a . 16 | /// 17 | public JsonFileConfigLoader() 18 | : this(Path.Combine(Assembly.GetExecutingAssembly().Location, $"../../../../../{DefaultFileName}")) 19 | { 20 | } 21 | 22 | /// 23 | /// Creates an instance of a . 24 | /// 25 | /// The location of the file that contains the settings. 26 | public JsonFileConfigLoader(string fileName) 27 | { 28 | this.FileName = fileName; 29 | } 30 | 31 | public static IApplicationConfigLoader Default { get; } = new JsonFileConfigLoader(); 32 | 33 | /// 34 | /// Gets or sets the location of the file that contains the settings. 35 | /// 36 | public string FileName { get; set; } 37 | 38 | /// 39 | /// Loads test configuration from a json file. 40 | /// 41 | /// An instance. 42 | /// Thrown when the json settings file does not exist. 43 | public IApplicationConfig Load() 44 | { 45 | FileInfo fileInfo = new FileInfo(this.FileName); 46 | if (!fileInfo.Exists) 47 | { 48 | throw new FileNotFoundException($"The JSON test application configuration file was not found at location {fileInfo.FullName}.", fileName: this.FileName); 49 | } 50 | 51 | string content = File.ReadAllText(fileInfo.FullName); 52 | ApplicationConfig result = JsonConvert.DeserializeObject(content); 53 | return result; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /PusherServer.Tests/PusherServer.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | net45 6 | true 7 | false 8 | false 9 | 10 | 11 | 12 | ..\PusherServer.snk 13 | 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 | -------------------------------------------------------------------------------- /PusherServer.Tests/RestfulClient/AuthenticationRequestFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using PusherServer.RestfulClient; 3 | using PusherServer.Tests.RestfulClient.Fakes; 4 | 5 | namespace PusherServer.Tests.RestfulClient 6 | { 7 | [TestFixture] 8 | public class When_creating_an_authenticated_request_for_pusher 9 | { 10 | private AuthenticatedRequestFactory _authenticatedRequestFactory; 11 | private MyTestObject _requestParameters; 12 | private MyTestObject _requestBody; 13 | 14 | private TestObjectFactory _factory; 15 | 16 | [OneTimeSetUp] 17 | public void Setup() 18 | { 19 | // Arrange 20 | _factory = new TestObjectFactory(); 21 | 22 | _authenticatedRequestFactory = new AuthenticatedRequestFactory("test_app_key", "test_app_id", "test_app_secret"); 23 | _requestParameters = _factory.Create("Test Property 1", 2, true); 24 | _requestBody = _factory.Create("Test Property 4", 5, false); 25 | } 26 | 27 | [Test] 28 | public void then_the_request_should_be_made_with_no_parameters() 29 | { 30 | // Act 31 | var request = _authenticatedRequestFactory.Build(PusherMethod.GET, "/testPath"); 32 | 33 | // Assert 34 | Assert.IsNotNull(request); 35 | Assert.AreEqual(PusherMethod.GET, request.Method); 36 | StringAssert.StartsWith(@"/apps/test_app_id/testPath?", request.ResourceUri); 37 | StringAssert.Contains(@"auth_version=1.0", request.ResourceUri); 38 | StringAssert.Contains(@"auth_key=test_app_key", request.ResourceUri); 39 | StringAssert.Contains(@"auth_timestamp=", request.ResourceUri); 40 | } 41 | 42 | [Test] 43 | public void then_the_request_should_be_made_with_parameters_from_a_source_object() 44 | { 45 | // Act 46 | var request = _authenticatedRequestFactory.Build(PusherMethod.GET, "/testPath", _requestParameters); 47 | 48 | // Assert 49 | Assert.IsNotNull(request); 50 | Assert.AreEqual(PusherMethod.GET, request.Method); 51 | StringAssert.StartsWith(@"/apps/test_app_id/testPath?", request.ResourceUri); 52 | StringAssert.Contains(@"auth_version=1.0", request.ResourceUri); 53 | StringAssert.Contains(@"auth_key=test_app_key", request.ResourceUri); 54 | StringAssert.Contains(@"auth_timestamp=", request.ResourceUri); 55 | StringAssert.Contains(@"Property1=Test Property 1&Property2=2&Property3=True", request.ResourceUri); 56 | } 57 | 58 | [Test] 59 | public void then_the_request_should_be_made_with_a_content_object() 60 | { 61 | // Act 62 | var request = _authenticatedRequestFactory.Build(PusherMethod.POST, "/testPath", requestBody: _requestBody); 63 | 64 | // Assert 65 | Assert.IsNotNull(request); 66 | Assert.AreEqual(PusherMethod.POST, request.Method); 67 | StringAssert.StartsWith(@"/apps/test_app_id/testPath?", request.ResourceUri); 68 | StringAssert.Contains(@"auth_version=1.0", request.ResourceUri); 69 | StringAssert.Contains(@"auth_key=test_app_key", request.ResourceUri); 70 | StringAssert.Contains(@"auth_timestamp=", request.ResourceUri); 71 | StringAssert.Contains(@"auth_signature=", request.ResourceUri); 72 | } 73 | 74 | [Test] 75 | public void then_the_request_should_be_made_with_parameters_from_a_source_object_and_a_content_object() 76 | { 77 | // Act 78 | var request = _authenticatedRequestFactory.Build(PusherMethod.POST, "/testPath", _requestParameters, _requestBody); 79 | 80 | // Assert 81 | Assert.IsNotNull(request); 82 | Assert.AreEqual(PusherMethod.POST, request.Method); 83 | StringAssert.StartsWith(@"/apps/test_app_id/testPath?", request.ResourceUri); 84 | StringAssert.Contains(@"auth_version=1.0", request.ResourceUri); 85 | StringAssert.Contains(@"auth_key=test_app_key", request.ResourceUri); 86 | StringAssert.Contains(@"auth_timestamp=", request.ResourceUri); 87 | StringAssert.Contains(@"auth_signature=", request.ResourceUri); 88 | StringAssert.Contains(@"Property1=Test Property 1&Property2=2&Property3=True", request.ResourceUri); 89 | StringAssert.Contains(@"body_md5=41db1761b31df9dc02b2811b38c010d4", request.ResourceUri); 90 | } 91 | 92 | [Test] 93 | public void then_the_request_should_return_the_body_as_a_string() 94 | { 95 | // Act 96 | var request = _authenticatedRequestFactory.Build(PusherMethod.POST, "/testPath", _requestParameters, _requestBody); 97 | 98 | // Assert 99 | Assert.IsNotNull(request); 100 | Assert.AreEqual(PusherMethod.POST, request.Method); 101 | StringAssert.Contains("{\"Property1\":\"Test Property 4\",\"Property2\":5,\"Property3\":false}", request.GetContentAsJsonString()); 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /PusherServer.Tests/RestfulClient/Fakes/MyTestObject.cs: -------------------------------------------------------------------------------- 1 | namespace PusherServer.Tests.RestfulClient.Fakes 2 | { 3 | internal class MyTestObject 4 | { 5 | public string Property1 { get; set; } 6 | public int Property2 { get; set; } 7 | public bool Property3 { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /PusherServer.Tests/RestfulClient/Fakes/TestObjectFactory.cs: -------------------------------------------------------------------------------- 1 | namespace PusherServer.Tests.RestfulClient.Fakes 2 | { 3 | internal class TestObjectFactory 4 | { 5 | public MyTestObject Create(string property1Value, int property2Value, bool property3Value) 6 | { 7 | return new MyTestObject 8 | { 9 | Property1 = property1Value, 10 | Property2 = property2Value, 11 | Property3 = property3Value 12 | }; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /PusherServer.Tests/RestfulClient/Fakes/TestOccupied.cs: -------------------------------------------------------------------------------- 1 | namespace PusherServer.Tests.RestfulClient.Fakes 2 | { 3 | internal class TestOccupied 4 | { 5 | public bool Occupied { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /PusherServer.Tests/RestfulClient/PusherRestClientTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using NUnit.Framework; 3 | using PusherServer.RestfulClient; 4 | using PusherServer.Tests.RestfulClient.Fakes; 5 | 6 | namespace PusherServer.Tests.RestfulClient 7 | { 8 | [TestFixture] 9 | public class When_making_a_request 10 | { 11 | [Test] 12 | public async Task then_the_get_request_should_be_made_with_a_valid_resource() 13 | { 14 | var factory = new AuthenticatedRequestFactory(Config.AppKey, Config.AppId, Config.AppSecret); 15 | var request = factory.Build(PusherMethod.GET, "/channels/newRestClient"); 16 | 17 | var client = new PusherRestClient($"http://{Config.HttpHost}", "pusher-http-dotnet", Pusher.VERSION); 18 | var response = await client.ExecuteGetAsync(request).ConfigureAwait(false); 19 | 20 | Assert.IsNotNull(response); 21 | Assert.IsFalse(response.Data.Occupied); 22 | Assert.AreEqual(30d, client.Timeout.TotalSeconds); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /PusherServer.Tests/RestfulClient/PusherRestRequestTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using PusherServer.RestfulClient; 4 | using PusherServer.Tests.RestfulClient.Fakes; 5 | 6 | namespace PusherServer.Tests.RestfulClient 7 | { 8 | [TestFixture] 9 | public class When_using_a_pusher_rest_request 10 | { 11 | private TestObjectFactory _factory; 12 | 13 | [OneTimeSetUp] 14 | public void Setup() 15 | { 16 | _factory = new TestObjectFactory(); 17 | } 18 | 19 | [Test] 20 | public void then_the_request_should_return_the_resource() 21 | { 22 | // Act 23 | var request = new PusherRestRequest("testUrl"); 24 | 25 | // Assert 26 | Assert.IsNotNull(request.ResourceUri); 27 | StringAssert.AreEqualIgnoringCase("testUrl", request.ResourceUri); 28 | } 29 | 30 | [Test] 31 | public void then_the_request_should_return_the_body_as_a_string_when_present() 32 | { 33 | // Arrange 34 | var request = new PusherRestRequest("testUrl"); 35 | 36 | // Act 37 | request.Body = _factory.Create("Test Property 1", 2, true); 38 | var jsonString = request.GetContentAsJsonString(); 39 | 40 | // Assert 41 | Assert.IsNotNull(request); 42 | StringAssert.Contains("{\"Property1\":\"Test Property 1\",\"Property2\":2,\"Property3\":true}", jsonString); 43 | } 44 | 45 | [Test] 46 | public void then_the_request_should_return_the_body_as_null_when_not_present() 47 | { 48 | // Arrange 49 | var request = new PusherRestRequest("testUrl"); 50 | 51 | // Act 52 | var jsonString = request.GetContentAsJsonString(); 53 | 54 | // Assert 55 | Assert.IsNotNull(request); 56 | Assert.IsNull(jsonString); 57 | } 58 | 59 | [Test] 60 | public void then_the_request_should_throw_an_exception_when_given_an_unpopulated_resource_uri() 61 | { 62 | // Arrange 63 | ArgumentNullException caughtException = null; 64 | 65 | // Act 66 | try 67 | { 68 | var request = new PusherRestRequest(null); 69 | } 70 | catch (ArgumentNullException ex) 71 | { 72 | caughtException = ex; 73 | } 74 | 75 | // Assert 76 | Assert.IsNotNull(caughtException); 77 | StringAssert.AreEqualIgnoringCase($"The resource URI must be a populated string{Environment.NewLine}Parameter name: resourceUri", caughtException.Message); 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /PusherServer.Tests/UnitTests/Authenticate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | using NUnit.Framework; 4 | using PusherServer.Exceptions; 5 | using PusherServer.Tests.Helpers; 6 | 7 | //// 8 | // This file tests the deprecated PusherServer.IPusher.Authenticate method. 9 | //// 10 | 11 | namespace PusherServer.Tests.UnitTests 12 | { 13 | [TestFixture] 14 | public class When_authenticating_a_private_channel 15 | { 16 | IPusher _pusher; 17 | 18 | [OneTimeSetUp] 19 | public void Setup() 20 | { 21 | _pusher = new Pusher(Config.AppId, Config.AppKey, Config.AppSecret); 22 | } 23 | 24 | [Test] 25 | public void the_auth_response_is_valid() 26 | { 27 | string channelName = "my-channel"; 28 | string socketId = "123.456"; 29 | 30 | string expectedAuthString = Config.AppKey + ":" + CreateSignedString(channelName, socketId); 31 | 32 | IAuthenticationData result = _pusher.Authenticate(channelName, socketId); 33 | Assert.AreEqual(expectedAuthString, result.auth); 34 | Assert.IsNull(result.shared_secret, nameof(result.shared_secret)); 35 | } 36 | 37 | [Test] 38 | [ExpectedException(typeof(SocketIdFormatException))] 39 | public void socket_id_cannot_contain_colon_prefix() 40 | { 41 | _pusher.Authenticate("private-test", ":444.444"); 42 | } 43 | 44 | [Test] 45 | [ExpectedException(typeof(SocketIdFormatException))] 46 | public void socket_id_cannot_contain_colon_suffix() 47 | { 48 | _pusher.Authenticate("private-test", "444.444:"); 49 | } 50 | 51 | [Test] 52 | [ExpectedException(typeof(SocketIdFormatException))] 53 | public void socket_id_cannot_contain_letters_suffix() 54 | { 55 | _pusher.Authenticate("private-test", "444.444a"); 56 | } 57 | 58 | [Test] 59 | [ExpectedException(typeof(SocketIdFormatException))] 60 | public void socket_id_must_contain_a_period_point() 61 | { 62 | _pusher.Authenticate("private-test", "444"); 63 | } 64 | 65 | [Test] 66 | [ExpectedException(typeof(SocketIdFormatException))] 67 | public void socket_id_must_not_contain_newline_prefix() 68 | { 69 | _pusher.Authenticate("private-test", "\n444.444"); 70 | } 71 | 72 | [Test] 73 | [ExpectedException(typeof(SocketIdFormatException))] 74 | public void socket_id_must_not_contain_newline_suffix() 75 | { 76 | _pusher.Authenticate("private-test", "444.444\n"); 77 | } 78 | 79 | [Test] 80 | [ExpectedException(typeof(SocketIdFormatException))] 81 | public void socket_id_must_not_be_empty_string() 82 | { 83 | _pusher.Authenticate("private-test", string.Empty); 84 | } 85 | 86 | [Test] 87 | [ExpectedException(typeof(ChannelNameFormatException))] 88 | public void channel_must_not_have_trailing_colon() 89 | { 90 | AuthWithChannelName("private-channel:"); 91 | } 92 | [Test] 93 | [ExpectedException(typeof(ChannelNameFormatException))] 94 | public void channel_name_must_not_have_leading_colon() 95 | { 96 | AuthWithChannelName(":private-channel"); 97 | } 98 | 99 | [Test] 100 | [ExpectedException(typeof(ChannelNameFormatException))] 101 | public void channel_name_must_not_have_leading_colon_newline() 102 | { 103 | AuthWithChannelName(":\nprivate-channel"); 104 | } 105 | 106 | [Test] 107 | [ExpectedException(typeof(ChannelNameFormatException))] 108 | public void channel_name_must_not_have_trailing_colon_newline() 109 | { 110 | AuthWithChannelName("private-channel\n:"); 111 | } 112 | 113 | [Test] 114 | [ExpectedException(typeof(ChannelNameLengthExceededException))] 115 | public void channel_names_must_not_exceed_allowed_length() 116 | { 117 | var channelName = new String('a', ValidationHelper.CHANNEL_NAME_MAX_LENGTH + 1); 118 | AuthWithChannelName(channelName); 119 | } 120 | 121 | private void AuthWithChannelName(string channelName) 122 | { 123 | _pusher.Authenticate(channelName, "123.456"); 124 | } 125 | 126 | private string CreateSignedString(string channelName, string socketId) 127 | { 128 | // null for presence data 129 | var stringToSign = socketId + ":" + channelName; 130 | return CryptoHelper.GetHmac256(Config.AppSecret, stringToSign); 131 | } 132 | } 133 | 134 | [TestFixture] 135 | public class When_authenticating_a_private_encrypted_channel 136 | { 137 | [Test] 138 | public void the_auth_response_is_valid() 139 | { 140 | string channelName = "private-encrypted-channel"; 141 | Pusher pusher = new Pusher(Config.AppId, Config.AppKey, Config.AppSecret, new PusherOptions 142 | { 143 | EncryptionMasterKey = DataHelper.GenerateEncryptionMasterKey(), 144 | }); 145 | 146 | IAuthenticationData result = pusher.Authenticate(channelName, "123.456"); 147 | 148 | Assert.IsNotNull(result, nameof(IAuthenticationData)); 149 | Assert.IsNotNull(result.shared_secret, nameof(result.shared_secret)); 150 | Assert.IsNotNull(Convert.FromBase64String(result.shared_secret)); 151 | } 152 | 153 | [Test] 154 | [ExpectedException(typeof(EncryptionMasterKeyException))] 155 | public void encryption_master_key_should_be_defined() 156 | { 157 | string channelName = "private-encrypted-channel"; 158 | Pusher pusher = new Pusher(Config.AppId, Config.AppKey, Config.AppSecret); 159 | pusher.Authenticate(channelName, "123.456"); 160 | } 161 | 162 | 163 | [Test] 164 | [ExpectedException(typeof(EncryptionMasterKeyException))] 165 | public void encryption_master_key_should_be_well_defined() 166 | { 167 | string channelName = "private-encrypted-channel"; 168 | Pusher pusher = new Pusher(Config.AppId, Config.AppKey, Config.AppSecret, new PusherOptions 169 | { 170 | EncryptionMasterKey = new byte[] { 1, 2 }, 171 | }); 172 | pusher.Authenticate(channelName, "123.456"); 173 | } 174 | } 175 | 176 | [TestFixture] 177 | public class When_authenticating_a_presence_channel 178 | { 179 | private IPusher _pusher; 180 | 181 | [OneTimeSetUp] 182 | public void Setup() 183 | { 184 | _pusher = new Pusher(Config.AppId, Config.AppKey, Config.AppSecret); 185 | } 186 | 187 | [Test] 188 | [ExpectedException(typeof(ArgumentNullException))] 189 | public void null_presence_data_throw_Exception() 190 | { 191 | string channelName = "my-channel"; 192 | string socketId = "some_socket_id"; 193 | 194 | _pusher.Authenticate(channelName, socketId, null); 195 | } 196 | 197 | [Test] 198 | public void the_auth_response_is_valid() 199 | { 200 | string channelName = "my-channel"; 201 | string socketId = "123.456"; 202 | 203 | PresenceChannelData data = new PresenceChannelData() 204 | { 205 | user_id = "unique_user_id", 206 | user_info = new { twitter_id = "@leggetter" } 207 | }; 208 | string presenceJson = DefaultSerializer.Default.Serialize(data); 209 | 210 | string expectedAuthString = Config.AppKey + ":" + CreateSignedString(channelName, socketId, presenceJson); 211 | 212 | IAuthenticationData result = _pusher.Authenticate(channelName, socketId, data); 213 | Assert.AreEqual(expectedAuthString, result.auth); 214 | Assert.IsNull(result.shared_secret, nameof(result.shared_secret)); 215 | } 216 | 217 | [Test] 218 | public void channel_data_is_encoded_as_JSON() 219 | { 220 | string channelName = "my-channel"; 221 | string socketId = "123.456"; 222 | 223 | PresenceChannelData data = new PresenceChannelData() 224 | { 225 | user_id = "unique_user_id", 226 | user_info = new { twitter_id = "@leggetter" } 227 | }; 228 | 229 | string expectedChannelData = DefaultSerializer.Default.Serialize(data); 230 | 231 | IAuthenticationData result = _pusher.Authenticate(channelName, socketId, data); 232 | Assert.AreEqual(expectedChannelData, result.channel_data); 233 | } 234 | 235 | private string CreateSignedString(string channelName, string socketId, string presenceJson) 236 | { 237 | var stringToSign = socketId + ":" + channelName + ":" + presenceJson; 238 | return CryptoHelper.GetHmac256(Config.AppSecret, stringToSign); 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /PusherServer.Tests/UnitTests/AuthenticateUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | using NUnit.Framework; 4 | using PusherServer.Exceptions; 5 | using PusherServer.Tests.Helpers; 6 | 7 | namespace PusherServer.Tests.UnitTests 8 | { 9 | [TestFixture] 10 | public class When_authenticating_a_user 11 | { 12 | private IPusher _pusher; 13 | 14 | [OneTimeSetUp] 15 | public void Setup() 16 | { 17 | _pusher = new Pusher(Config.AppId, Config.AppKey, Config.AppSecret); 18 | } 19 | 20 | [Test] 21 | [ExpectedException(typeof(ArgumentNullException))] 22 | public void null_user_data_throw_Exception() 23 | { 24 | string socketId = "some_socket_id"; 25 | 26 | _pusher.AuthenticateUser(socketId, null); 27 | } 28 | 29 | [Test] 30 | public void the_auth_response_is_valid() 31 | { 32 | string socketId = "123.456"; 33 | 34 | UserData userData = new UserData() 35 | { 36 | id = "unique_user_id", 37 | }; 38 | string userDataJson = DefaultSerializer.Default.Serialize(userData); 39 | 40 | string expectedAuthString = Config.AppKey + ":" + CreateSignedString(socketId, userDataJson); 41 | 42 | IUserAuthenticationResponse result = _pusher.AuthenticateUser(socketId, userData); 43 | Assert.AreEqual(expectedAuthString, result.auth); 44 | Assert.AreEqual(userDataJson, result.user_data); 45 | } 46 | 47 | [Test] 48 | public void with_watchlist_the_auth_response_is_valid() 49 | { 50 | string socketId = "123.456"; 51 | 52 | UserData userData = new UserData() 53 | { 54 | id = "unique_user_id", 55 | watchlist = new string[] { "user1", "user2" }, 56 | user_info = new { twitter_id = "@leggetter" } 57 | }; 58 | string userDataJson = DefaultSerializer.Default.Serialize(userData); 59 | 60 | string expectedAuthString = Config.AppKey + ":" + CreateSignedString(socketId, userDataJson); 61 | 62 | IUserAuthenticationResponse result = _pusher.AuthenticateUser(socketId, userData); 63 | Assert.AreEqual(expectedAuthString, result.auth); 64 | Assert.AreEqual(userDataJson, result.user_data); 65 | } 66 | 67 | [Test] 68 | public void with_userinfo_the_auth_response_is_valid() 69 | { 70 | string socketId = "123.456"; 71 | 72 | UserData userData = new UserData() 73 | { 74 | id = "unique_user_id", 75 | user_info = new { twitter_id = "@leggetter" } 76 | }; 77 | string userDataJson = DefaultSerializer.Default.Serialize(userData); 78 | 79 | string expectedAuthString = Config.AppKey + ":" + CreateSignedString(socketId, userDataJson); 80 | 81 | IUserAuthenticationResponse result = _pusher.AuthenticateUser(socketId, userData); 82 | Assert.AreEqual(expectedAuthString, result.auth); 83 | Assert.AreEqual(userDataJson, result.user_data); 84 | } 85 | 86 | [Test] 87 | public void with_userinfo_and_watchlist_the_auth_response_is_valid() 88 | { 89 | string socketId = "123.456"; 90 | 91 | UserData userData = new UserData() 92 | { 93 | id = "unique_user_id", 94 | watchlist = new string[] { "user1", "user2" }, 95 | user_info = new { twitter_id = "@leggetter" } 96 | }; 97 | string userDataJson = DefaultSerializer.Default.Serialize(userData); 98 | 99 | string expectedAuthString = Config.AppKey + ":" + CreateSignedString(socketId, userDataJson); 100 | 101 | IUserAuthenticationResponse result = _pusher.AuthenticateUser(socketId, userData); 102 | Assert.AreEqual(expectedAuthString, result.auth); 103 | Assert.AreEqual(userDataJson, result.user_data); 104 | } 105 | 106 | private string CreateSignedString(string socketId, string userDataJson) 107 | { 108 | var stringToSign = socketId + "::user::" + userDataJson; 109 | return CryptoHelper.GetHmac256(Config.AppSecret, stringToSign); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /PusherServer.Tests/UnitTests/AuthorizeChannel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | using NUnit.Framework; 4 | using PusherServer.Exceptions; 5 | using PusherServer.Tests.Helpers; 6 | 7 | namespace PusherServer.Tests.UnitTests 8 | { 9 | [TestFixture] 10 | public class When_authorizing_a_private_channel 11 | { 12 | IPusher _pusher; 13 | 14 | [OneTimeSetUp] 15 | public void Setup() 16 | { 17 | _pusher = new Pusher(Config.AppId, Config.AppKey, Config.AppSecret); 18 | } 19 | 20 | [Test] 21 | public void the_auth_response_is_valid() 22 | { 23 | string channelName = "my-channel"; 24 | string socketId = "123.456"; 25 | 26 | string expectedAuthString = Config.AppKey + ":" + CreateSignedString(channelName, socketId); 27 | 28 | IChannelAuthorizationResponse result = _pusher.AuthorizeChannel(channelName, socketId); 29 | Assert.AreEqual(expectedAuthString, result.auth); 30 | Assert.IsNull(result.shared_secret, nameof(result.shared_secret)); 31 | } 32 | 33 | [Test] 34 | [ExpectedException(typeof(SocketIdFormatException))] 35 | public void socket_id_cannot_contain_colon_prefix() 36 | { 37 | _pusher.AuthorizeChannel("private-test", ":444.444"); 38 | } 39 | 40 | [Test] 41 | [ExpectedException(typeof(SocketIdFormatException))] 42 | public void socket_id_cannot_contain_colon_suffix() 43 | { 44 | _pusher.AuthorizeChannel("private-test", "444.444:"); 45 | } 46 | 47 | [Test] 48 | [ExpectedException(typeof(SocketIdFormatException))] 49 | public void socket_id_cannot_contain_letters_suffix() 50 | { 51 | _pusher.AuthorizeChannel("private-test", "444.444a"); 52 | } 53 | 54 | [Test] 55 | [ExpectedException(typeof(SocketIdFormatException))] 56 | public void socket_id_must_contain_a_period_point() 57 | { 58 | _pusher.AuthorizeChannel("private-test", "444"); 59 | } 60 | 61 | [Test] 62 | [ExpectedException(typeof(SocketIdFormatException))] 63 | public void socket_id_must_not_contain_newline_prefix() 64 | { 65 | _pusher.AuthorizeChannel("private-test", "\n444.444"); 66 | } 67 | 68 | [Test] 69 | [ExpectedException(typeof(SocketIdFormatException))] 70 | public void socket_id_must_not_contain_newline_suffix() 71 | { 72 | _pusher.AuthorizeChannel("private-test", "444.444\n"); 73 | } 74 | 75 | [Test] 76 | [ExpectedException(typeof(SocketIdFormatException))] 77 | public void socket_id_must_not_be_empty_string() 78 | { 79 | _pusher.AuthorizeChannel("private-test", string.Empty); 80 | } 81 | 82 | [Test] 83 | [ExpectedException(typeof(ChannelNameFormatException))] 84 | public void channel_must_not_have_trailing_colon() 85 | { 86 | AuthWithChannelName("private-channel:"); 87 | } 88 | [Test] 89 | [ExpectedException(typeof(ChannelNameFormatException))] 90 | public void channel_name_must_not_have_leading_colon() 91 | { 92 | AuthWithChannelName(":private-channel"); 93 | } 94 | 95 | [Test] 96 | [ExpectedException(typeof(ChannelNameFormatException))] 97 | public void channel_name_must_not_have_leading_colon_newline() 98 | { 99 | AuthWithChannelName(":\nprivate-channel"); 100 | } 101 | 102 | [Test] 103 | [ExpectedException(typeof(ChannelNameFormatException))] 104 | public void channel_name_must_not_have_trailing_colon_newline() 105 | { 106 | AuthWithChannelName("private-channel\n:"); 107 | } 108 | 109 | [Test] 110 | [ExpectedException(typeof(ChannelNameLengthExceededException))] 111 | public void channel_names_must_not_exceed_allowed_length() 112 | { 113 | var channelName = new String('a', ValidationHelper.CHANNEL_NAME_MAX_LENGTH + 1); 114 | AuthWithChannelName(channelName); 115 | } 116 | 117 | private void AuthWithChannelName(string channelName) 118 | { 119 | _pusher.AuthorizeChannel(channelName, "123.456"); 120 | } 121 | 122 | private string CreateSignedString(string channelName, string socketId) 123 | { 124 | // null for presence data 125 | var stringToSign = socketId + ":" + channelName; 126 | return CryptoHelper.GetHmac256(Config.AppSecret, stringToSign); 127 | } 128 | } 129 | 130 | [TestFixture] 131 | public class When_authorizing_a_private_encrypted_channel 132 | { 133 | [Test] 134 | public void the_auth_response_is_valid() 135 | { 136 | string channelName = "private-encrypted-channel"; 137 | Pusher pusher = new Pusher(Config.AppId, Config.AppKey, Config.AppSecret, new PusherOptions 138 | { 139 | EncryptionMasterKey = DataHelper.GenerateEncryptionMasterKey(), 140 | }); 141 | 142 | IChannelAuthorizationResponse result = pusher.AuthorizeChannel(channelName, "123.456"); 143 | 144 | Assert.IsNotNull(result, nameof(IAuthenticationData)); 145 | Assert.IsNotNull(result.shared_secret, nameof(result.shared_secret)); 146 | Assert.IsNotNull(Convert.FromBase64String(result.shared_secret)); 147 | } 148 | 149 | [Test] 150 | [ExpectedException(typeof(EncryptionMasterKeyException))] 151 | public void encryption_master_key_should_be_defined() 152 | { 153 | string channelName = "private-encrypted-channel"; 154 | Pusher pusher = new Pusher(Config.AppId, Config.AppKey, Config.AppSecret); 155 | pusher.AuthorizeChannel(channelName, "123.456"); 156 | } 157 | 158 | 159 | [Test] 160 | [ExpectedException(typeof(EncryptionMasterKeyException))] 161 | public void encryption_master_key_should_be_well_defined() 162 | { 163 | string channelName = "private-encrypted-channel"; 164 | Pusher pusher = new Pusher(Config.AppId, Config.AppKey, Config.AppSecret, new PusherOptions 165 | { 166 | EncryptionMasterKey = new byte[] { 1, 2 }, 167 | }); 168 | pusher.AuthorizeChannel(channelName, "123.456"); 169 | } 170 | } 171 | 172 | [TestFixture] 173 | public class When_authorizing_a_presence_channel 174 | { 175 | private IPusher _pusher; 176 | 177 | [OneTimeSetUp] 178 | public void Setup() 179 | { 180 | _pusher = new Pusher(Config.AppId, Config.AppKey, Config.AppSecret); 181 | } 182 | 183 | [Test] 184 | [ExpectedException(typeof(ArgumentNullException))] 185 | public void null_presence_data_throw_Exception() 186 | { 187 | string channelName = "my-channel"; 188 | string socketId = "some_socket_id"; 189 | 190 | _pusher.AuthorizeChannel(channelName, socketId, null); 191 | } 192 | 193 | [Test] 194 | public void the_auth_response_is_valid() 195 | { 196 | string channelName = "my-channel"; 197 | string socketId = "123.456"; 198 | 199 | PresenceChannelData data = new PresenceChannelData() 200 | { 201 | user_id = "unique_user_id", 202 | user_info = new { twitter_id = "@leggetter" } 203 | }; 204 | string presenceJson = DefaultSerializer.Default.Serialize(data); 205 | 206 | string expectedAuthString = Config.AppKey + ":" + CreateSignedString(channelName, socketId, presenceJson); 207 | 208 | IChannelAuthorizationResponse result = _pusher.AuthorizeChannel(channelName, socketId, data); 209 | Assert.AreEqual(expectedAuthString, result.auth); 210 | Assert.IsNull(result.shared_secret, nameof(result.shared_secret)); 211 | } 212 | 213 | [Test] 214 | public void channel_data_is_encoded_as_JSON() 215 | { 216 | string channelName = "my-channel"; 217 | string socketId = "123.456"; 218 | 219 | PresenceChannelData data = new PresenceChannelData() 220 | { 221 | user_id = "unique_user_id", 222 | user_info = new { twitter_id = "@leggetter" } 223 | }; 224 | 225 | string expectedChannelData = DefaultSerializer.Default.Serialize(data); 226 | 227 | IChannelAuthorizationResponse result = _pusher.AuthorizeChannel(channelName, socketId, data); 228 | Assert.AreEqual(expectedChannelData, result.channel_data); 229 | } 230 | 231 | private string CreateSignedString(string channelName, string socketId, string presenceJson) 232 | { 233 | var stringToSign = socketId + ":" + channelName + ":" + presenceJson; 234 | return CryptoHelper.GetHmac256(Config.AppSecret, stringToSign); 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /PusherServer.Tests/UnitTests/ChannelsList.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Newtonsoft.Json; 3 | 4 | namespace PusherServer.Tests.UnitTests 5 | { 6 | [TestFixture] 7 | public class When_deserializing_to_a_ChannelsList 8 | { 9 | string json = "{\"channels\":" + 10 | "{" + 11 | "\"test_channel\": {}," + 12 | "\"presence-channel\": { \"user_count\": \"300\" } " + 13 | "}" + 14 | "}"; 15 | 16 | [Test] 17 | public void It_should_make_the_channels_accessible_by_name_via_the_indexer() 18 | { 19 | ChannelsList list = JsonConvert.DeserializeObject(json); 20 | 21 | Assert.IsNotNull(list["test_channel"]); 22 | } 23 | 24 | [Test] 25 | public void It_should_be_possible_to_access_channel_information() 26 | { 27 | ChannelsList list = JsonConvert.DeserializeObject(json); 28 | 29 | Assert.AreEqual( list["presence-channel"]["user_count"], "300" ); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /PusherServer.Tests/UnitTests/Get.cs: -------------------------------------------------------------------------------- 1 | using NSubstitute; 2 | using NUnit.Framework; 3 | using PusherServer.RestfulClient; 4 | using PusherServer.Tests.Helpers; 5 | using System.Threading.Tasks; 6 | 7 | namespace PusherServer.Tests.UnitTests 8 | { 9 | [TestFixture] 10 | public class When_using_async_Get_to_retrieve_a_list_of_application_channels 11 | { 12 | private IPusher _pusher; 13 | private IPusherRestClient _subPusherClient; 14 | private IApplicationConfig _config; 15 | 16 | [SetUp] 17 | public void Setup() 18 | { 19 | _subPusherClient = Substitute.For(); 20 | 21 | IPusherOptions options = new PusherOptions() 22 | { 23 | RestClient = _subPusherClient 24 | }; 25 | 26 | _config = new ApplicationConfig 27 | { 28 | AppId = "test-app-id", 29 | AppKey = "test-app-key", 30 | AppSecret = "test-app-secret", 31 | }; 32 | 33 | _pusher = new Pusher(_config.AppId, _config.AppKey, _config.AppSecret, options); 34 | } 35 | 36 | [Test] 37 | public async Task url_is_in_expected_format() 38 | { 39 | await _pusher.GetAsync("/channels").ConfigureAwait(false); 40 | 41 | #pragma warning disable 4014 42 | _subPusherClient.Received().ExecuteGetAsync( 43 | #pragma warning restore 4014 44 | Arg.Is( 45 | x => x.ResourceUri.StartsWith("/apps/" + _config.AppId + "/channels") 46 | ) 47 | ); 48 | } 49 | 50 | [Test] 51 | public async Task GET_request_is_made() 52 | { 53 | await _pusher.GetAsync("/channels").ConfigureAwait(false); 54 | 55 | #pragma warning disable 4014 56 | _subPusherClient.Received().ExecuteGetAsync( 57 | #pragma warning restore 4014 58 | Arg.Is( 59 | x => x.Method == PusherMethod.GET 60 | ) 61 | ); 62 | } 63 | 64 | [Test] 65 | public async Task additional_parameters_should_be_added_to_query_string() 66 | { 67 | await _pusher.GetAsync("/channels", new { filter_by_prefix = "presence-", info = "user_count" }).ConfigureAwait(false); 68 | 69 | #pragma warning disable 4014 70 | _subPusherClient.Received().ExecuteGetAsync( 71 | #pragma warning restore 4014 72 | Arg.Is( 73 | x => x.ResourceUri.Contains("&filter_by_prefix=presence-") && 74 | x.ResourceUri.Contains("&info=user_count") 75 | ) 76 | ); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /PusherServer.Tests/UnitTests/GetResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net; 3 | using System.Net.Http; 4 | using NSubstitute; 5 | using NUnit.Framework; 6 | 7 | namespace PusherServer.Tests.UnitTests 8 | { 9 | [TestFixture] 10 | public class When_using_GetResult_to_deserialise_a_rest_response 11 | { 12 | [Test] 13 | public void GetResult_should_gracefully_handle_a_deserialisation_exception() 14 | { 15 | var stubRestResponse = Substitute.For(); 16 | stubRestResponse.Content = new StringContent("not json"); 17 | stubRestResponse.StatusCode = HttpStatusCode.BadRequest; 18 | 19 | var getResult = new GetResult(stubRestResponse, "not json"); 20 | 21 | Assert.AreEqual(HttpStatusCode.BadRequest, getResult.StatusCode); 22 | StringAssert.IsMatch("not json", getResult.Body); 23 | Assert.IsNull(getResult.Data); 24 | Assert.AreEqual(stubRestResponse, getResult.Response); 25 | } 26 | 27 | [Test] 28 | public void GetResult_should_deserialise_a_valid_json_response() 29 | { 30 | var stubRestResponse = Substitute.For(); 31 | stubRestResponse.Content = new StringContent("[\"string1\", \"string2\", \"string3\"]"); 32 | 33 | var getResult = new GetResult>(stubRestResponse, "[\"string1\", \"string2\", \"string3\"]"); 34 | 35 | Assert.AreNotEqual(HttpStatusCode.BadRequest, getResult.StatusCode); 36 | Assert.AreEqual(3, getResult.Data.Count); 37 | StringAssert.IsMatch("string2", getResult.Data[1]); 38 | Assert.AreEqual(stubRestResponse, getResult.Response); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /PusherServer.Tests/UnitTests/Pusher.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using PusherServer.Tests.Helpers; 3 | using System; 4 | 5 | namespace PusherServer.Tests.UnitTests 6 | { 7 | [TestFixture] 8 | public class When_creating_a_new_Pusher_instance 9 | { 10 | [Test] 11 | [ExpectedException(typeof(ArgumentException))] 12 | public void appId_cannot_be_null() 13 | { 14 | new Pusher(null, "app-key", "app-secret"); 15 | } 16 | 17 | [Test] 18 | [ExpectedException(typeof(ArgumentException))] 19 | public void appKey_cannot_be_null() 20 | { 21 | new Pusher("app-id", null, "app-secret"); 22 | } 23 | 24 | [Test] 25 | [ExpectedException(typeof(ArgumentException))] 26 | public void appSecret_cannot_be_null() 27 | { 28 | new Pusher("app-id", "app-key", null); 29 | } 30 | 31 | [Test] 32 | [ExpectedException(typeof(ArgumentException))] 33 | public void appId_cannot_be_empty_string() 34 | { 35 | new Pusher(string.Empty, "app-key", "app-secret"); 36 | } 37 | 38 | [Test] 39 | [ExpectedException(typeof(ArgumentException))] 40 | public void appKey_cannot_empty_string() 41 | { 42 | new Pusher("app-id", string.Empty, "app-secret"); 43 | } 44 | 45 | [Test] 46 | [ExpectedException(typeof(ArgumentException))] 47 | public void appSecret_cannot_be_empty_string() 48 | { 49 | new Pusher("app-id", "app-key", string.Empty); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /PusherServer.Tests/UnitTests/PusherOptions.cs: -------------------------------------------------------------------------------- 1 | using NSubstitute; 2 | using NUnit.Framework; 3 | using PusherServer.Tests.Helpers; 4 | using System; 5 | 6 | namespace PusherServer.Tests.UnitTests 7 | { 8 | [TestFixture] 9 | public class When_creating_a_new_PusherOptions_instance 10 | { 11 | [Test] 12 | public void a_default_RestClient_should_be_used_if_one_is_not_set_on_PusherOptions_parameter() 13 | { 14 | var options = new PusherOptions(); 15 | Assert.IsNotNull(options.RestClient); 16 | } 17 | 18 | [Test] 19 | public void Port_defaults_to_80() 20 | { 21 | var options = new PusherOptions(); 22 | Assert.AreEqual(80, options.Port); 23 | } 24 | 25 | [Test] 26 | public void when_Encrypted_option_is_set_Port_is_changed_to_443() 27 | { 28 | var options = new PusherOptions() { Encrypted = true }; 29 | Assert.AreEqual(443, options.Port); 30 | } 31 | 32 | [Test] 33 | public void when_Encrypted_option_is_set_Port_is_changed_to_443_unless_Port_has_already_been_modified() 34 | { 35 | var options = new PusherOptions() { Port = 90 }; 36 | options.Encrypted = true; 37 | Assert.AreEqual(90, options.Port); 38 | } 39 | 40 | [Test] 41 | public void the_default_options_should_be_used_to_create_the_base_url_when_no_settings_are_changed() 42 | { 43 | var options = new PusherOptions(); 44 | 45 | StringAssert.IsMatch("http://api.pusherapp.com", options.GetBaseUrl().AbsoluteUri); 46 | } 47 | 48 | [Test] 49 | public void the_default_cluster_is_null() 50 | { 51 | var options = new PusherOptions(); 52 | 53 | Assert.AreEqual(null, options.Cluster); 54 | } 55 | 56 | [Test] 57 | public void the_default_encrypted_options_should_be_used_to_create_the_base_url_when_encrypted_is_true() 58 | { 59 | var options = new PusherOptions(); 60 | options.Encrypted = true; 61 | 62 | StringAssert.IsMatch("https://api.pusherapp.com", options.GetBaseUrl().AbsoluteUri); 63 | } 64 | 65 | [Test] 66 | public void the_new_cluster_should_be_used_to_create_the_base_url() 67 | { 68 | var options= new PusherOptions(); 69 | options.Cluster = "eu"; 70 | 71 | StringAssert.IsMatch("http://api-eu.pusher.com", options.GetBaseUrl().AbsoluteUri); 72 | } 73 | 74 | [Test] 75 | public void the_new_port_should_be_used_to_create_the_base_url() 76 | { 77 | var options = new PusherOptions(); 78 | options.Port = 100; 79 | 80 | StringAssert.IsMatch("http://api.pusherapp.com:100", options.GetBaseUrl().AbsoluteUri); 81 | } 82 | 83 | [Test] 84 | public void the_new_port_should_be_used_to_create_the_base_url_when_its_encrypted() 85 | { 86 | var options = new PusherOptions(); 87 | options.Encrypted = true; 88 | options.Port = 100; 89 | 90 | StringAssert.IsMatch("https://api.pusherapp.com:100", options.GetBaseUrl().AbsoluteUri); 91 | } 92 | 93 | [Test] 94 | public void the_new_cluster_should_be_used_to_create_the_base_url_when_its_encrypted_and_has_a_custom_port() 95 | { 96 | var options = new PusherOptions(); 97 | options.Encrypted = true; 98 | options.Cluster = "eu"; 99 | options.Port = 100; 100 | 101 | StringAssert.IsMatch("https://api-eu.pusher.com:100", options.GetBaseUrl().AbsoluteUri); 102 | } 103 | 104 | [Test] 105 | public void the_cluster_should_be_ignored_when_host_name_is_set_first() 106 | { 107 | var options = new PusherOptions(); 108 | options.HostName = "api.my.domain.com"; 109 | options.Cluster = "eu"; 110 | 111 | StringAssert.IsMatch("http://api.my.domain.com", options.GetBaseUrl().AbsoluteUri); 112 | Assert.AreEqual(null, options.Cluster); 113 | } 114 | 115 | [Test] 116 | public void the_cluster_should_be_ignored_when_host_name_is_set_after() 117 | { 118 | var options = new PusherOptions(); 119 | 120 | options.Cluster = "eu"; 121 | StringAssert.IsMatch("http://api-eu.pusher.com", options.GetBaseUrl().AbsoluteUri); 122 | 123 | options.HostName = "api.my.domain.com"; 124 | StringAssert.IsMatch("http://api.my.domain.com", options.GetBaseUrl().AbsoluteUri); 125 | Assert.AreEqual(null, options.Cluster); 126 | } 127 | 128 | [Test] 129 | [ExpectedException(typeof(FormatException))] 130 | public void https_scheme_is_not_allowed_when_setting_host() 131 | { 132 | var httpsOptions = new PusherOptions(); 133 | httpsOptions.HostName = "https://api.pusherapp.com"; 134 | } 135 | 136 | [Test] 137 | [ExpectedException(typeof(FormatException))] 138 | public void http_scheme_is_not_allowed_when_setting_host() 139 | { 140 | var httpsOptions = new PusherOptions(); 141 | httpsOptions.HostName = "http://api.pusherapp.com"; 142 | } 143 | 144 | [Test] 145 | [ExpectedException(typeof(FormatException))] 146 | public void ftp_scheme_is_not_allowed_when_setting_host() 147 | { 148 | var httpsOptions = new PusherOptions(); 149 | httpsOptions.HostName = "ftp://api.pusherapp.com"; 150 | } 151 | 152 | [Test] 153 | public void the_json_deserialiser_should_be_the_default_one_when_none_is_set() 154 | { 155 | var options = new PusherOptions(); 156 | 157 | Assert.IsInstanceOf(options.JsonDeserializer); 158 | } 159 | 160 | [Test] 161 | public void the_json_deserialiser_should_be_the_supplied_one_when_set() 162 | { 163 | var options = new PusherOptions(); 164 | options.JsonDeserializer = new FakeDeserialiser(); 165 | 166 | Assert.IsInstanceOf(options.JsonDeserializer); 167 | } 168 | 169 | [Test] 170 | public void the_json_deserialiser_should_be_the_supplied_one_when_set_with_a_custom_and_the_set_to_null() 171 | { 172 | var options = new PusherOptions(); 173 | options.JsonDeserializer = new FakeDeserialiser(); 174 | options.JsonDeserializer = null; 175 | 176 | Assert.IsInstanceOf(options.JsonDeserializer); 177 | } 178 | 179 | [Test] 180 | public void the_json_serialiser_should_be_the_default_one_when_none_is_set() 181 | { 182 | var options = new PusherOptions(); 183 | 184 | Assert.IsInstanceOf(options.JsonSerializer); 185 | } 186 | 187 | [Test] 188 | public void the_json_serialiser_should_be_the_supplied_one_when_set() 189 | { 190 | var options = new PusherOptions(); 191 | options.JsonSerializer = new FakeSerialiser(); 192 | 193 | Assert.IsInstanceOf(options.JsonSerializer); 194 | } 195 | 196 | [Test] 197 | public void the_json_serialiser_should_be_the_default_one_when_set_with_a_custom_and_the_set_to_null() 198 | { 199 | var options = new PusherOptions(); 200 | options.JsonSerializer = new FakeSerialiser(); 201 | options.JsonSerializer = null; 202 | 203 | Assert.IsInstanceOf(options.JsonSerializer); 204 | } 205 | 206 | private class FakeDeserialiser : IDeserializeJsonStrings 207 | { 208 | public T Deserialize(string stringToDeserialize) 209 | { 210 | throw new NotImplementedException(); 211 | } 212 | } 213 | 214 | private class FakeSerialiser : ISerializeObjectsToJson 215 | { 216 | public string Serialize(object objectToSerialize) 217 | { 218 | throw new NotImplementedException(); 219 | } 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /PusherServer.Tests/UnitTests/RawBodySerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | 4 | namespace PusherServer.Tests.UnitTests 5 | { 6 | [TestFixture] 7 | public class When_using_RawBodySerializer_to_serialize_a_rest_response 8 | { 9 | [Test] 10 | public void RawBodySerialiser_should_return_an_empty_string_when_given_a_null_body() 11 | { 12 | var rawBodySerializer = new RawBodySerializer(); 13 | var response = rawBodySerializer.Serialize(null); 14 | 15 | Assert.IsEmpty(response); 16 | } 17 | 18 | [Test] 19 | public void RawBodySerializer_should_return_the_pass_in_string_when_it_is_populated() 20 | { 21 | var populatedString = "populated string"; 22 | 23 | var rawBodySerializer = new RawBodySerializer(); 24 | var response = rawBodySerializer.Serialize(populatedString); 25 | 26 | StringAssert.IsMatch(populatedString, response); 27 | } 28 | 29 | [Test] 30 | public void RawBodySerializer_should_throw_an_exception_when_given_any_object_instead_of_a_string_to_serialize() 31 | { 32 | var anObject = new object(); 33 | ArgumentException caughtException = null; 34 | 35 | try 36 | { 37 | var rawBodySerializer = new RawBodySerializer(); 38 | rawBodySerializer.Serialize(anObject); 39 | } 40 | catch (ArgumentException ex) 41 | { 42 | caughtException = ex; 43 | } 44 | 45 | Assert.IsNotNull(caughtException); 46 | StringAssert.IsMatch("The RawBodySerializer only supports strings for messages. The body type was: ", caughtException.Message); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /PusherServer.Tests/UnitTests/TriggerResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using NSubstitute; 5 | using NUnit.Framework; 6 | using PusherServer.Exceptions; 7 | using PusherServer.Tests.Helpers; 8 | 9 | namespace PusherServer.Tests.UnitTests 10 | { 11 | public class TriggerResultHelper 12 | { 13 | public static string TRIGGER_RESPONSE_JSON = "{" + 14 | "\"event_ids\": {" + 15 | "\"ch1\": \"ch1_event_id\"," + 16 | "\"ch2\": \"ch2_event_id\"," + 17 | "\"ch3\": \"ch3_event_id\"" + 18 | "}" + 19 | "}"; 20 | 21 | public static string TRIGGER_RESPONSE_WITH_CHANNEL_ATTRIBUTES = "{" + 22 | "\"channels\": {" + 23 | "\"any-channel\": {" + 24 | "\"user_count\": 1," + 25 | "\"subscription_count\": 2" + 26 | "}" + 27 | "}," + 28 | "\"event_ids\": {" + 29 | "\"any-channel\": \"test-id\"" + 30 | "}" + 31 | "}"; 32 | } 33 | 34 | [TestFixture] 35 | public class When_initialisation_a_TriggerEvent 36 | { 37 | private HttpResponseMessage V7_PROTOCOL_SUCCESSFUL_RESPONSE; 38 | 39 | [OneTimeSetUp] 40 | public void FixtureSetUp() 41 | { 42 | V7_PROTOCOL_SUCCESSFUL_RESPONSE = Substitute.For(); 43 | V7_PROTOCOL_SUCCESSFUL_RESPONSE.Content = new StringContent("{}"); 44 | V7_PROTOCOL_SUCCESSFUL_RESPONSE.StatusCode = HttpStatusCode.OK; 45 | } 46 | 47 | [Test] 48 | [ExpectedException(typeof (ArgumentNullException))] 49 | public void it_should_not_accept_a_null_response() 50 | { 51 | new TriggerResult(null, null); 52 | } 53 | 54 | [Test] 55 | public void it_should_treat_a_v7_protocol_200_response_as_a_successful_request() 56 | { 57 | var triggerResult = new TriggerResult(V7_PROTOCOL_SUCCESSFUL_RESPONSE, "{}"); 58 | Assert.AreEqual(HttpStatusCode.OK, triggerResult.StatusCode); 59 | } 60 | 61 | [Test] 62 | public void it_should_have_no_event_id_value_when_a_v7_protocol_200_response_is_returned() 63 | { 64 | var triggerResult = new TriggerResult(V7_PROTOCOL_SUCCESSFUL_RESPONSE, "{}"); 65 | Assert.AreEqual(0, triggerResult.EventIds.Count); 66 | } 67 | 68 | [Test] 69 | [ExpectedException(typeof (TriggerResponseException))] 70 | public void it_should_treat_non_JSON_content_in_the_request_body_as_a_failed_request() 71 | { 72 | HttpResponseMessage response = Substitute.For(); 73 | response.Content = new StringContent("FISH"); 74 | response.StatusCode = HttpStatusCode.OK; 75 | 76 | new TriggerResult(response, "FISH"); 77 | try 78 | { 79 | new TriggerResult(response, "FISH"); 80 | Assert.Fail("Exception was not thrown"); 81 | } 82 | catch (TriggerResponseException ex) 83 | { 84 | Assert.IsTrue(ex.Message.Contains("FISH"), "exception message includes response"); 85 | } 86 | } 87 | 88 | [Test] 89 | [ExpectedException(typeof (NotSupportedException))] 90 | public void it_should_not_be_possible_to_change_EventIds() 91 | { 92 | HttpResponseMessage response = Substitute.For(); 93 | response.StatusCode = HttpStatusCode.OK; 94 | response.Content = new StringContent(TriggerResultHelper.TRIGGER_RESPONSE_JSON); 95 | var triggerResult = new TriggerResult(response, TriggerResultHelper.TRIGGER_RESPONSE_JSON); 96 | 97 | triggerResult.EventIds.Add("fish", "pie"); 98 | } 99 | 100 | [Test] 101 | public void it_should_parse_channel_attributes() 102 | { 103 | HttpResponseMessage response = Substitute.For(); 104 | response.StatusCode = HttpStatusCode.OK; 105 | response.Content = new StringContent(TriggerResultHelper.TRIGGER_RESPONSE_WITH_CHANNEL_ATTRIBUTES); 106 | 107 | var triggerResult = new TriggerResult(response, TriggerResultHelper.TRIGGER_RESPONSE_WITH_CHANNEL_ATTRIBUTES); 108 | 109 | Assert.AreEqual(2, triggerResult.ChannelAttributes["any-channel"].subscription_count); 110 | Assert.AreEqual(1, triggerResult.ChannelAttributes["any-channel"].user_count); 111 | Assert.AreEqual(1, triggerResult.EventIds.Count); 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /PusherServer.Tests/UnitTests/WebHook.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | 4 | namespace PusherServer.Tests.UnitTests 5 | { 6 | [TestFixture] 7 | public class when_creating_a_webhook 8 | { 9 | private static string GenerateValidSignature(string secret, string stringToSign) 10 | { 11 | return CryptoHelper.GetHmac256(secret, stringToSign); 12 | } 13 | 14 | static string secret = "some_crazy_secret"; 15 | 16 | static string validBody = "{\"time_ms\": 1327078148132, \"events\": [{\"name\": \"channel_occupied\", \"channel\": \"test_channel\" }]}"; 17 | static string validSignature = GenerateValidSignature(secret, validBody); 18 | 19 | [Test] 20 | public void the_WebHook_will_be_valid_if_all_params_are_as_expected() 21 | { 22 | var webHook = new WebHook(secret, validSignature, validBody); 23 | 24 | Assert.IsTrue(webHook.IsValid); 25 | } 26 | 27 | [Test] 28 | public void the_event_name_can_be_retrieved_from_the_WebHook() 29 | { 30 | var webHook = new WebHook(secret, validSignature, validBody); 31 | 32 | Assert.AreEqual("channel_occupied", webHook.Events[0]["name"]); 33 | } 34 | 35 | [Test] 36 | public void the_channel_name_can_be_retrieved_from_the_WebHook() 37 | { 38 | var webHook = new WebHook(secret, validSignature, validBody); 39 | 40 | Assert.AreEqual("test_channel", webHook.Events[0]["channel"]); 41 | } 42 | 43 | [Test] 44 | public void the_WebHook_can_contain_multiple_events() 45 | { 46 | var body = "{\"time_ms\": 1327078148132, \"events\": " + 47 | "[" + 48 | "{\"name\": \"channel_occupied\", \"channel\": \"test_channel\" }," + 49 | "{\"name\": \"channel_vacated\", \"channel\": \"test_channel2\" }" + 50 | "]}"; 51 | 52 | var webHook = new WebHook(secret, validSignature, body); 53 | Assert.AreEqual("test_channel", webHook.Events[0]["channel"]); 54 | Assert.AreEqual("channel_occupied", webHook.Events[0]["name"]); 55 | 56 | Assert.AreEqual("test_channel2", webHook.Events[1]["channel"]); 57 | Assert.AreEqual("channel_vacated", webHook.Events[1]["name"]); 58 | } 59 | 60 | [Test] 61 | public void the_WebHook_will_throw_exception_if_secret_is_null() 62 | { 63 | ArgumentException caughtException = null; 64 | 65 | try 66 | { 67 | new WebHook(null, validSignature, validBody); 68 | } 69 | catch (ArgumentException ex) 70 | { 71 | caughtException = ex; 72 | } 73 | 74 | StringAssert.IsMatch("A secret must be provided" + Environment.NewLine + "Parameter name: secret", caughtException.Message); 75 | } 76 | 77 | [Test] 78 | public void the_WebHook_will_throw_exception_if_secret_is_empty() 79 | { 80 | ArgumentException caughtException = null; 81 | 82 | try 83 | { 84 | new WebHook(string.Empty, validSignature, validBody); 85 | } 86 | catch (ArgumentException ex) 87 | { 88 | caughtException = ex; 89 | } 90 | 91 | StringAssert.IsMatch("A secret must be provided" + Environment.NewLine + "Parameter name: secret", caughtException.Message); 92 | } 93 | 94 | [Test] 95 | public void the_WebHook_will_be_invalid_if_they_signature_is_null() 96 | { 97 | var webHook = new WebHook(secret, null, validBody); 98 | 99 | Assert.IsFalse(webHook.IsValid); 100 | Assert.AreEqual(2, webHook.ValidationErrors.Length); 101 | StringAssert.IsMatch(@"The supplied signature to check was null or empty\. A signature to check must be provided\.", webHook.ValidationErrors[0]); 102 | StringAssert.IsMatch(@"The signature did not validate\. Expected \. Got 003a63ce4da20830c4fecffb63d5b3944b64989b6458e15b26e08e244f758954", webHook.ValidationErrors[1]); 103 | } 104 | 105 | [Test] 106 | public void the_WebHook_will_be_invalid_if_they_signature_is_empty() 107 | { 108 | var webHook = new WebHook(secret, string.Empty, validBody); 109 | 110 | Assert.IsFalse(webHook.IsValid); 111 | Assert.AreEqual(2, webHook.ValidationErrors.Length); 112 | StringAssert.IsMatch(@"The supplied signature to check was null or empty\. A signature to check must be provided\.", webHook.ValidationErrors[0]); 113 | StringAssert.IsMatch(@"The signature did not validate\. Expected \. Got 003a63ce4da20830c4fecffb63d5b3944b64989b6458e15b26e08e244f758954", webHook.ValidationErrors[1]); 114 | } 115 | 116 | [Test] 117 | public void the_WebHook_will_be_invalid_if_they_body_is_null() 118 | { 119 | var webHook = new WebHook(secret, validSignature, null); 120 | 121 | Assert.IsFalse(webHook.IsValid); 122 | Assert.AreEqual(1, webHook.ValidationErrors.Length); 123 | StringAssert.IsMatch(@"The supplied body to check was null or empty\. A body to check must be provided\.", webHook.ValidationErrors[0]); 124 | } 125 | 126 | [Test] 127 | public void the_WebHook_will_be_invalid_if_they_body_is_empty() 128 | { 129 | var webHook = new WebHook(secret, validSignature, string.Empty); 130 | 131 | Assert.IsFalse(webHook.IsValid); 132 | Assert.AreEqual(1, webHook.ValidationErrors.Length); 133 | StringAssert.IsMatch(@"The supplied body to check was null or empty\. A body to check must be provided\.", webHook.ValidationErrors[0]); 134 | } 135 | 136 | [Test] 137 | public void the_WebHook_will_not_be_valid_when_given_invalid_JSON_for_the_body() 138 | { 139 | var secret = "1c9c753dddfd049dd7f1"; 140 | var body = "{Invalid JSON}"; 141 | var expectedSignature = GenerateValidSignature(secret, body); 142 | 143 | var webHook = new WebHook(secret, expectedSignature, body); 144 | 145 | Assert.IsFalse(webHook.IsValid); 146 | StringAssert.IsMatch("Exception occurred parsing the body as JSON: .*", webHook.ValidationErrors[0]); 147 | } 148 | 149 | [Test] 150 | public void the_WebHook_will_be_valid_given_alternative_values() 151 | { 152 | var signature = "851f492bab8f7652a2e4c82cd0212d97b4e678edf085c06bf640ed45ee7b1169"; 153 | var secret = "1c9c753dddfd049dd7f1"; 154 | var body = "{\"time_ms\":1423778833207,\"events\":[{\"channel\":\"test_channel\",\"name\":\"channel_occupied\"}]}"; 155 | 156 | var webHook = new WebHook(secret, signature, body); 157 | Assert.IsTrue(webHook.IsValid); 158 | } 159 | 160 | [Test] 161 | public void the_WebHook_time_in_ms_is_correctly_parsed() 162 | { 163 | var fakeMillis = "1423850522000"; 164 | var expectedDate = new DateTime(2015, 2, 13, 18, 2, 2, DateTimeKind.Utc); 165 | var secret = "1c9c753dddfd049dd7f1"; 166 | var body = "{\"time_ms\":" + fakeMillis + ",\"events\":[{\"channel\":\"test_channel\",\"name\":\"channel_occupied\"}]}"; 167 | var expectedSignature = GenerateValidSignature(secret, body); 168 | 169 | var webHook = new WebHook(secret, expectedSignature, body); 170 | Assert.AreEqual(expectedDate, webHook.Time); 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /PusherServer.public.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pusher/pusher-http-dotnet/363dcf0bf68a486daae4674f8702f5f9eafb1e35/PusherServer.public.snk -------------------------------------------------------------------------------- /PusherServer/BatchEvent.cs: -------------------------------------------------------------------------------- 1 | namespace PusherServer 2 | { 3 | /// 4 | /// Represents an event that is part of a batch trigger, for the purposes of serialisation 5 | /// 6 | internal class BatchEvent 7 | { 8 | /// 9 | /// The name of the event 10 | /// 11 | public string name { get; set; } 12 | 13 | /// 14 | /// The event data 15 | /// 16 | public string data { get; set; } 17 | 18 | /// 19 | /// The channel the event should be triggered on. 20 | /// 21 | public string channel { get; set; } 22 | 23 | /// 24 | /// The id of a socket to be excluded from receiving the event. 25 | /// 26 | public string socket_id { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /PusherServer/BatchTriggerBody.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace PusherServer 3 | { 4 | /// 5 | /// Represents the payload to be sent when triggering events 6 | /// 7 | class BatchTriggerBody 8 | { 9 | public BatchEvent[] batch { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /PusherServer/ChannelAttributes.cs: -------------------------------------------------------------------------------- 1 | namespace PusherServer 2 | { 3 | /// 4 | /// Channel attributes as returned in the trigger event endpoint 5 | /// 6 | public class ChannelAttributes 7 | { 8 | /// 9 | /// Number of distinct users currently subscribed to each channel (a single user may be subscribed many times, but will only count as one) 10 | /// 11 | public int user_count { get; set; } 12 | 13 | /// 14 | /// Number of connections currently subscribed to each channel. This attribute is not available by default. To enable it, navigate to your Channels dashboard, find the app you are working on, and click App Settings. 15 | /// 16 | public int subscription_count { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /PusherServer/ChannelAuthorizationResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace PusherServer 4 | { 5 | [DataContract] 6 | class ChannelAuthorizationResponse: IChannelAuthorizationResponse, IAuthenticationData 7 | { 8 | private readonly string _appKey; 9 | private readonly string _appSecret; 10 | private readonly string _channelName; 11 | private readonly string _socketId; 12 | private readonly PresenceChannelData _presenceData; 13 | private readonly byte[] _key; 14 | 15 | public ChannelAuthorizationResponse(string appKey, string appSecret, string channelName, string socketId) 16 | { 17 | ValidationHelper.ValidateChannelName(channelName); 18 | ValidationHelper.ValidateSocketId(socketId); 19 | 20 | _appKey = appKey; 21 | _appSecret = appSecret; 22 | _channelName = channelName; 23 | _socketId = socketId; 24 | } 25 | 26 | public ChannelAuthorizationResponse(string appKey, string appSecret, string channelName, string socketId, byte[] masterEncryptionKey) 27 | : this(appKey, appSecret, channelName, socketId) 28 | { 29 | ValidationHelper.ValidateEncryptionMasterKey(masterEncryptionKey); 30 | 31 | _key = masterEncryptionKey; 32 | } 33 | 34 | public ChannelAuthorizationResponse(string appKey, string appSecret, string channelName, string socketId, PresenceChannelData presenceData) 35 | : this(appKey, appSecret, channelName, socketId) 36 | { 37 | _presenceData = presenceData; 38 | } 39 | 40 | [DataMember(Name = "auth", IsRequired = true)] 41 | public string auth 42 | { 43 | get 44 | { 45 | var stringToSign = _socketId + ":" + _channelName; 46 | if (_presenceData != null) 47 | { 48 | var presenceJson = DefaultSerializer.Default.Serialize(_presenceData); 49 | stringToSign += ":" + presenceJson; 50 | } 51 | 52 | return _appKey + ":" + CryptoHelper.GetHmac256(_appSecret, stringToSign); 53 | } 54 | } 55 | 56 | /// 57 | /// Double encoded JSON containing presence channel user information. 58 | /// 59 | [DataMember(Name = "channel_data", IsRequired = false, EmitDefaultValue = false)] 60 | public string channel_data 61 | { 62 | get 63 | { 64 | string json = null; 65 | if (_presenceData != null) 66 | { 67 | json = DefaultSerializer.Default.Serialize(_presenceData); 68 | } 69 | return json; 70 | } 71 | } 72 | 73 | [DataMember(Name = "shared_secret", IsRequired = false, EmitDefaultValue = false)] 74 | public string shared_secret 75 | { 76 | get 77 | { 78 | string result = null; 79 | if (_key != null) 80 | { 81 | result = CryptoHelper.GenerateSharedSecret(_key, _channelName); 82 | } 83 | 84 | return result; 85 | } 86 | } 87 | 88 | public string ToJson() 89 | { 90 | return ToString(); 91 | } 92 | 93 | public override string ToString() 94 | { 95 | return DefaultSerializer.Default.Serialize(this); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /PusherServer/ChannelDataEncrypter.cs: -------------------------------------------------------------------------------- 1 | using NaCl; 2 | using System.Security.Cryptography; 3 | using System.Text; 4 | 5 | namespace PusherServer 6 | { 7 | internal class ChannelDataEncrypter : IChannelDataEncrypter 8 | { 9 | EncryptedChannelData IChannelDataEncrypter.EncryptData(string channelName, string jsonData, byte[] encryptionMasterKey) 10 | { 11 | ValidationHelper.ValidateChannelName(channelName); 12 | ValidationHelper.ValidateEncryptionMasterKey(encryptionMasterKey); 13 | 14 | EncryptedChannelData result = null; 15 | if (jsonData != null) 16 | { 17 | byte[] sharedSecret = CryptoHelper.GenerateSharedSecretHash(encryptionMasterKey, channelName); 18 | using (XSalsa20Poly1305 secretBox = new XSalsa20Poly1305(sharedSecret)) 19 | using (RandomNumberGenerator random = RandomNumberGenerator.Create()) 20 | { 21 | byte[] message = Encoding.UTF8.GetBytes(jsonData); 22 | byte[] cipher = new byte[message.Length + XSalsa20Poly1305.TagLength]; 23 | byte[] nonce = new byte[XSalsa20Poly1305.NonceLength]; 24 | random.GetBytes(nonce); 25 | secretBox.Encrypt(cipher, message, nonce); 26 | result = new EncryptedChannelData(nonceBytes: nonce, encrypted: cipher); 27 | } 28 | } 29 | 30 | return result; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /PusherServer/ChannelsList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | using System.Collections.Generic; 4 | 5 | namespace PusherServer 6 | { 7 | /* 8 | {"channels":{"test_channel":{}}} 9 | */ 10 | 11 | /// 12 | /// A list of Channels received from the Pusher Server 13 | /// 14 | [DataContract] 15 | public class ChannelsList 16 | { 17 | private Dictionary> _channelsInfo = null; 18 | 19 | /// 20 | /// Gets or sets the Channel Info for a given Channel Name 21 | /// 22 | /// 23 | /// A string representing the Channel Info 24 | public Dictionary this[string channelName] 25 | { 26 | get 27 | { 28 | return _channelsInfo[channelName]; 29 | } 30 | set 31 | { 32 | _channelsInfo[channelName] = value; 33 | } 34 | } 35 | 36 | /// 37 | /// Gets or sets all the Channel Info 38 | /// 39 | [DataMember(Name = "channels")] 40 | public Dictionary> Channels 41 | { 42 | get 43 | { 44 | return _channelsInfo; 45 | } 46 | set 47 | { 48 | if (_channelsInfo != null) 49 | { 50 | throw new InvalidOperationException("Channels should only be set as part of deserialization."); 51 | } 52 | _channelsInfo = value; 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /PusherServer/CryptoHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | 7 | namespace PusherServer 8 | { 9 | internal class CryptoHelper 10 | { 11 | internal static string GetMd5Hash(string jsonData) 12 | { 13 | using (var md5 = MD5.Create()) 14 | { 15 | var result = md5.ComputeHash(Encoding.UTF8.GetBytes(jsonData)); 16 | return BytesToHex(result); 17 | } 18 | } 19 | 20 | private static string BytesToHex(IEnumerable byteArray) 21 | { 22 | return String.Concat(byteArray.Select(bytes => bytes.ToString("x2")).ToArray()); 23 | } 24 | 25 | internal static string GetHmac256(string secret, string toSign) 26 | { 27 | using (var hmacsha256 = new HMACSHA256(Encoding.UTF8.GetBytes(secret))) 28 | { 29 | var hash = hmacsha256.ComputeHash(Encoding.UTF8.GetBytes(toSign)); 30 | return BytesToHex(hash); 31 | } 32 | } 33 | 34 | internal static byte[] GenerateSharedSecretHash(byte[] key, string toSign) 35 | { 36 | using (var hmacsha256 = new HMACSHA256(key)) 37 | { 38 | return hmacsha256.ComputeHash(Encoding.UTF8.GetBytes(toSign)); 39 | } 40 | } 41 | 42 | internal static string GenerateSharedSecret(byte[] key, string toSign) 43 | { 44 | return Convert.ToBase64String(GenerateSharedSecretHash(key, toSign)); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /PusherServer/DefaultDeserializer.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace PusherServer 4 | { 5 | /// 6 | /// Default implmentation for deserializing an object 7 | /// 8 | public class DefaultDeserializer : IDeserializeJsonStrings 9 | { 10 | /// 11 | public T Deserialize(string stringToDeserialize) 12 | { 13 | return JsonConvert.DeserializeObject(stringToDeserialize); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /PusherServer/DefaultSerializer.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace PusherServer 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 | } -------------------------------------------------------------------------------- /PusherServer/EncryptedChannelData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherServer 4 | { 5 | internal class EncryptedChannelData 6 | { 7 | public EncryptedChannelData(byte[] nonceBytes, byte[] encrypted) 8 | { 9 | this.nonce = Convert.ToBase64String(nonceBytes); 10 | this.ciphertext = Convert.ToBase64String(encrypted); 11 | } 12 | 13 | public string nonce { get; set; } 14 | 15 | public string ciphertext { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /PusherServer/Event.cs: -------------------------------------------------------------------------------- 1 | namespace PusherServer 2 | { 3 | /// 4 | /// Represents an event for batch submission 5 | /// 6 | public class Event 7 | { 8 | /// 9 | /// The event name 10 | /// 11 | public string EventName { get; set; } 12 | 13 | /// 14 | /// The channel to which the event should be sent 15 | /// 16 | public string Channel { get; set; } 17 | 18 | /// 19 | /// An optional socket ID which should not receive the event 20 | /// 21 | public string SocketId { get; set; } 22 | 23 | /// 24 | /// The event data 25 | /// 26 | public object Data { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /PusherServer/EventIdData.cs: -------------------------------------------------------------------------------- 1 |  2 | using System.Collections.Generic; 3 | 4 | namespace PusherServer 5 | { 6 | /// 7 | /// Class used for handling the deserialisation of the Trigger HTTP response. 8 | /// 9 | public class EventIdData 10 | { 11 | private readonly Dictionary _eventIds = new Dictionary(); 12 | private readonly Dictionary _channelStates = new Dictionary(); 13 | 14 | /// 15 | /// Dictionary of channel name to event ID for the triggered event. 16 | /// 17 | public Dictionary event_ids 18 | { 19 | get 20 | { 21 | return _eventIds; 22 | } 23 | } 24 | 25 | /// 26 | /// Dictionary of channel name to channel attributes 27 | /// 28 | public Dictionary channels => _channelStates; 29 | } 30 | } -------------------------------------------------------------------------------- /PusherServer/Exceptions/ChannelNameFormatException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherServer.Exceptions 4 | { 5 | /// 6 | /// Thrown when an invalid channel name is used when triggering an event. 7 | /// 8 | public class ChannelNameFormatException : FormatException 9 | { 10 | /// 11 | /// Creates an instance of a . 12 | /// 13 | /// The invalid channel name that causes this exception. 14 | public ChannelNameFormatException(string actualValue) : 15 | base($"The channel name \"{actualValue}\" was not in the form: {ValidationHelper.CHANNEL_NAME_REGEX}") 16 | { 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /PusherServer/Exceptions/ChannelNameLengthExceededException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherServer.Exceptions 4 | { 5 | /// 6 | /// Thrown when a channel name is too long. 7 | /// 8 | public class ChannelNameLengthExceededException : ArgumentOutOfRangeException 9 | { 10 | /// 11 | /// Creates an instance of a . 12 | /// 13 | /// The name of the parameter that causes this exception. 14 | /// The length of the channel name that causes this exception. 15 | public ChannelNameLengthExceededException(string paramName, int actualValue) : 16 | base(paramName, actualValue, $"The length of the channel name is greater than the allowed {ValidationHelper.CHANNEL_NAME_MAX_LENGTH} characters.") 17 | { 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /PusherServer/Exceptions/EncryptionMasterKeyException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherServer.Exceptions 4 | { 5 | /// 6 | /// Thrown when an encryption master key is invalid. 7 | /// 8 | public class EncryptionMasterKeyException : ArgumentOutOfRangeException 9 | { 10 | /// 11 | /// Creates an instance of an . 12 | /// 13 | /// The name of the parameter that causes this exception. 14 | /// The length of the key that causes this exception. 15 | public EncryptionMasterKeyException(string paramName, int actualValue) 16 | : base(paramName, actualValue, $"The encryption master key is expected to be {ValidationHelper.ENCRYPTION_MASTER_KEY_LENGTH} bytes in length.") 17 | { 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /PusherServer/Exceptions/EventBatchSizeExceededException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherServer.Exceptions 4 | { 5 | /// 6 | /// Thrown when the number of events triggered in a batch exceeds the limit allowed. 7 | /// 8 | public class EventBatchSizeExceededException : ArgumentOutOfRangeException 9 | { 10 | /// 11 | /// Creates an instance of a . 12 | /// 13 | /// The name of the parameter that causes this exception. 14 | /// The batch size that causes this exception. 15 | public EventBatchSizeExceededException(string paramName, int actualValue) : 16 | base(paramName, actualValue, $"Only {ValidationHelper.MAX_BATCH_SIZE} events permitted per batch.") 17 | { 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /PusherServer/Exceptions/EventDataSizeExceededException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherServer.Exceptions 4 | { 5 | /// 6 | /// Thrown when the size of the Data for an exceeds the byte size limit. 7 | /// 8 | public class EventDataSizeExceededException : InvalidOperationException 9 | { 10 | /// 11 | /// Creates an instance of a . 12 | /// 13 | /// The maximum size in bytes allowed for event data. 14 | /// The invalid event data size in bytes that causes this exception. 15 | public EventDataSizeExceededException(int sizeLimitInBytes, int actualValue) : 16 | base($"The data content of this event exceeds the allowed maximum ({sizeLimitInBytes} bytes). The actual size is {actualValue} bytes.") 17 | { 18 | } 19 | 20 | /// 21 | /// Gets or sets the name of a channel associated with event data that has caused the exception. 22 | /// 23 | public string ChannelName { get; set; } 24 | 25 | /// 26 | /// Gets or sets the name of an event associated with data that has caused the exception. 27 | /// 28 | public string EventName { get; set; } 29 | } 30 | } -------------------------------------------------------------------------------- /PusherServer/Exceptions/SocketIdFormatException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherServer.Exceptions 4 | { 5 | /// 6 | /// Thrown when an invalid socket id is used when triggering an event. 7 | /// 8 | public class SocketIdFormatException : FormatException 9 | { 10 | /// 11 | /// Creates an instance of a . 12 | /// 13 | /// The invalid socket id that causes this exception. 14 | public SocketIdFormatException(string actualValue) : 15 | base($"The socket id \"{actualValue}\" was not in the form: {ValidationHelper.SOCKET_ID_REGEX}") 16 | { 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /PusherServer/Exceptions/TriggerResponseException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherServer.Exceptions 4 | { 5 | /// 6 | /// Thrown when problems are detected with the response from the Pusher trigger HTTP endpoint. 7 | /// 8 | public class TriggerResponseException : Exception 9 | { 10 | /// 11 | /// Create a new instance 12 | /// 13 | /// Description of the exception 14 | /// The inner exception 15 | public TriggerResponseException(string message, Exception innerException) : 16 | base(message, innerException) 17 | { 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /PusherServer/GetResult.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using Newtonsoft.Json; 3 | 4 | namespace PusherServer 5 | { 6 | /// 7 | /// Deserialised the result from a Rest Response 8 | /// 9 | /// The Type the Rest Response contains 10 | public class GetResult : RequestResult, IGetResult 11 | { 12 | /// 13 | /// Attempts to deserialise the data contained with a Rest Response 14 | /// 15 | /// The original response from the rest call 16 | /// the extracted content 17 | public GetResult(HttpResponseMessage response, string content) : base(response, content) 18 | { 19 | if (response.IsSuccessStatusCode) 20 | Data = JsonConvert.DeserializeObject(content); 21 | } 22 | 23 | /// 24 | /// Gets the data deserialised from the Rest Response 25 | /// 26 | public T Data { get; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /PusherServer/IAuthenticationData.cs: -------------------------------------------------------------------------------- 1 | namespace PusherServer 2 | { 3 | /// 4 | /// Interface for Authenticaton Data 5 | /// 6 | public interface IAuthenticationData : IChannelAuthorizationResponse 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /PusherServer/IChannelAuthorizationResponse.cs: -------------------------------------------------------------------------------- 1 | namespace PusherServer 2 | { 3 | /// 4 | /// Interface for Authenticaton Data 5 | /// 6 | public interface IChannelAuthorizationResponse 7 | { 8 | /// 9 | /// Gets the Authetication String 10 | /// 11 | string auth { get; } 12 | 13 | /// 14 | /// Double encoded JSON containing presence channel user information. 15 | /// 16 | string channel_data { get; } 17 | 18 | /// 19 | /// Gets the shared secret for an encrypted private channel. 20 | /// 21 | string shared_secret { get; } 22 | 23 | /// 24 | /// Returns a Json representation of the authentication data. 25 | /// 26 | /// 27 | string ToJson(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /PusherServer/IChannelDataEncrypter.cs: -------------------------------------------------------------------------------- 1 | namespace PusherServer 2 | { 3 | internal interface IChannelDataEncrypter 4 | { 5 | EncryptedChannelData EncryptData(string channelName, string jsonData, byte[] encryptionMasterKey); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /PusherServer/IDeserializeJsonStrings.cs: -------------------------------------------------------------------------------- 1 | namespace PusherServer 2 | { 3 | /// 4 | /// Contract that allows a JSON deserializer to be injected 5 | /// 6 | public interface IDeserializeJsonStrings 7 | { 8 | /// 9 | /// Deserialize a JSON string into an object 10 | /// 11 | /// The JSON string to be deserialized into an object instance 12 | /// A populated object 13 | T Deserialize(string stringToDeserialize); 14 | } 15 | } -------------------------------------------------------------------------------- /PusherServer/IGetResult.cs: -------------------------------------------------------------------------------- 1 | namespace PusherServer 2 | { 3 | /// 4 | /// The result of a GET HTTP request to the Pusher REST API. 5 | /// 6 | /// The object type that the data returned from the request should be deserialized to. 7 | public interface IGetResult: IRequestResult 8 | { 9 | /// 10 | /// Gets the data returned from the request in a deserialized form. 11 | /// 12 | /// 13 | /// The data. 14 | /// 15 | T Data { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /PusherServer/IPusher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace PusherServer 5 | { 6 | /// 7 | /// Provides access to functionality within the Pusher service such as Trigger to trigger events 8 | /// and authenticating subscription requests to private and presence channels. 9 | /// 10 | public interface IPusher 11 | { 12 | /// 13 | /// Triggers an event on the specified channels in the background. 14 | /// 15 | /// The name of the channel to trigger the event on 16 | /// The name of the event. 17 | /// The data to be sent with the event. The event payload. 18 | /// Additional options to be used when triggering the event. See . 19 | /// The result of the call to the REST API 20 | Task TriggerAsync(string channelName, string eventName, object data, ITriggerOptions options = null); 21 | 22 | /// 23 | /// Triggers an event on the specified channels in the background. 24 | /// 25 | /// The channels to trigger the event on 26 | /// The name of the event. 27 | /// The data to be sent with the event. The event payload. 28 | /// (Optional)Additional options to be used when triggering the event. See . 29 | /// The result of the call to the REST API 30 | Task TriggerAsync(string[] channelNames, string eventName, object data, ITriggerOptions options = null); 31 | 32 | /// 33 | /// Triggers the events in the passed in array asynchronously 34 | /// 35 | /// The events to trigger 36 | /// The result of the call to the REST API 37 | Task TriggerAsync(Event[] events); 38 | 39 | /// 40 | /// DEPRECATED: Use instead. 41 | /// Authorizes the subscription request for a private channel. 42 | /// 43 | /// Name of the channel to be authenticated. 44 | /// The socket id which uniquely identifies the connection attempting to subscribe to the channel. 45 | /// 46 | /// Authorization response where the required auth token can be accessed via 47 | /// The full response can be accessed via 48 | /// 49 | IAuthenticationData Authenticate(string channelName, string socketId); 50 | 51 | /// 52 | /// DEPRECATED: Use instead. 53 | /// Authorizes the subscription request for a presence channel. 54 | /// 55 | /// Name of the channel to be authenticated. 56 | /// The socket id which uniquely identifies the connection attempting to subscribe to the channel. 57 | /// Information about the user subscribing to the presence channel. 58 | /// If is null 59 | /// 60 | /// Authorization response where the required auth token can be accessed via 61 | /// The full response can be accessed via 62 | /// 63 | IAuthenticationData Authenticate(string channelName, string socketId, PresenceChannelData data); 64 | 65 | /// 66 | /// Authorizes the subscription request for a private channel. 67 | /// 68 | /// Name of the channel to be authenticated. 69 | /// The socket id which uniquely identifies the connection attempting to subscribe to the channel. 70 | /// 71 | /// Authorization response where the required auth token can be accessed via 72 | /// The full response can be accessed via 73 | /// 74 | IChannelAuthorizationResponse AuthorizeChannel(string channelName, string socketId); 75 | 76 | /// 77 | /// Authorizes the subscription request for a presence channel. 78 | /// 79 | /// Name of the channel to be authenticated. 80 | /// The socket id which uniquely identifies the connection attempting to subscribe to the channel. 81 | /// Information about the user subscribing to the presence channel. 82 | /// If is null 83 | /// 84 | /// Authorization response where the required auth token can be accessed via 85 | /// The full response can be accessed via 86 | /// 87 | IChannelAuthorizationResponse AuthorizeChannel(string channelName, string socketId, PresenceChannelData data); 88 | 89 | /// 90 | /// Authenticates a user to Pusher. 91 | /// 92 | /// The socket id which uniquely identifies the connection attempting to authenticate a user. 93 | /// Information about the user. 94 | /// If is null 95 | /// 96 | /// Authentication response where the required auth token can be accessed via 97 | /// The full response can be accessed via 98 | /// 99 | IUserAuthenticationResponse AuthenticateUser(string socketId, UserData userData); 100 | 101 | 102 | /// 103 | /// Makes an asynchronous GET request to the specified resource. Authentication is handled as part of the call. The data returned from the request is deserizlized to the object type defined by . 104 | /// 105 | /// 106 | /// The resource. 107 | /// Additional parameters to be sent as part of the request query string. 108 | /// The result of the GET request 109 | Task> GetAsync(string resource, object parameters = null); 110 | 111 | /// 112 | /// Handle an incoming WebHook and validate it. 113 | /// 114 | /// The signature of the incoming WebHook 115 | /// The body of the incoming Webhook request 116 | /// A WebHook helper. 117 | IWebHook ProcessWebHook(string signature, string body); 118 | 119 | /// 120 | /// Queries the Pusher API for the Users of a Presence Channel asynchronously 121 | /// 122 | /// The type of object that will be returned by the API 123 | /// The name of the channel to query 124 | /// The result of the Presence Channel Users query 125 | Task> FetchUsersFromPresenceChannelAsync(string channelName); 126 | 127 | /// 128 | /// Asynchronously queries the Pusher API for the state of a Channel 129 | /// 130 | /// The type of object that will be returned by the API 131 | /// The name of the channel to query 132 | /// An object containing a list of attributes to include in the query 133 | /// The result of the Channel State query 134 | Task> FetchStateForChannelAsync(string channelName, object info = null); 135 | 136 | /// 137 | /// Queries the Pusher API for the state of all channels based upon the info object 138 | /// 139 | /// The type of object that will be returned by the API 140 | /// An object containing a list of attributes to include in the query 141 | Task> FetchStateForChannelsAsync(object info); 142 | } 143 | } -------------------------------------------------------------------------------- /PusherServer/IPusherOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using PusherServer.RestfulClient; 3 | using PusherServer.Util; 4 | 5 | namespace PusherServer 6 | { 7 | /// 8 | /// Interface for Pusher Options 9 | /// 10 | public interface IPusherOptions 11 | { 12 | /// 13 | /// Gets or sets a value indicating whether calls to the Pusher REST API are over HTTP or HTTPS. 14 | /// 15 | /// 16 | /// true if encrypted; otherwise, false. 17 | /// 18 | bool Encrypted { get; set; } 19 | 20 | /// 21 | /// Gets or sets the REST API port that the HTTP calls will be made to. 22 | /// 23 | /// 24 | /// The port. 25 | /// 26 | int Port { get; set; } 27 | 28 | /// 29 | /// Gets or sets the Json Serializer 30 | /// 31 | ISerializeObjectsToJson JsonSerializer { get; set; } 32 | 33 | /// 34 | /// Gets or sets the Json Deserializer 35 | /// 36 | IDeserializeJsonStrings JsonDeserializer { get; set; } 37 | 38 | /// 39 | /// Gets or sets the Pusher rest client. Generally only expected to be used for testing. 40 | /// 41 | /// 42 | /// The pusher rest client. 43 | /// 44 | IPusherRestClient RestClient { get; set; } 45 | 46 | /// 47 | /// Gets or sets the Pusher rest client timeout. The default timeout is 30 seconds. 48 | /// 49 | TimeSpan RestClientTimeout { get; set; } 50 | 51 | /// 52 | /// The host of the HTTP API endpoint excluding the scheme e.g. api.pusherapp.com 53 | /// 54 | /// If a scheme is found at the start of the host value 55 | string HostName { get; set; } 56 | 57 | /// 58 | /// The cluster where the application was created, e.g. eu 59 | /// 60 | string Cluster { get; set; } 61 | 62 | /// 63 | /// Gets or sets the size limit for the Data property of an . 64 | /// This is normally 10KB but SDK customers can request a larger limit. 65 | /// 66 | /// 67 | /// If this value is specified each Event.Data field will be validated before triggering an event on the server. 68 | /// The validation check will happen client side rather than server side if this value is specified. 69 | /// 70 | int? BatchEventDataSizeLimit { get; set; } 71 | 72 | /// 73 | /// Gets or sets the to use for tracing debug messages. 74 | /// 75 | ITraceLogger TraceLogger { get; set; } 76 | 77 | /// 78 | /// Gets or sets the encryption master key. The key is required to be 32 bytes in length. 79 | /// 80 | byte[] EncryptionMasterKey { get; set; } 81 | 82 | /// 83 | /// Gets the base Url based on the set Options 84 | /// 85 | /// The constructed URL 86 | Uri GetBaseUrl(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /PusherServer/IRequestResult.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace PusherServer 4 | { 5 | /// 6 | /// Base interface for all Request Results 7 | /// 8 | public interface IRequestResult 9 | { 10 | /// 11 | /// Gets the Body from a Request Result 12 | /// 13 | string Body { get; } 14 | 15 | /// 16 | /// Gets the Status Code from a Request Result 17 | /// 18 | HttpStatusCode StatusCode { get; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PusherServer/ISerializeObjectsToJson.cs: -------------------------------------------------------------------------------- 1 | namespace PusherServer 2 | { 3 | /// 4 | /// Contract that allows a JSON serializer to be injected 5 | /// 6 | public interface ISerializeObjectsToJson 7 | { 8 | /// 9 | /// Serialize 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 | -------------------------------------------------------------------------------- /PusherServer/ITriggerOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PusherServer 4 | { 5 | /// 6 | /// Additional options that can be used when triggering an event. 7 | /// 8 | public interface ITriggerOptions 9 | { 10 | /// 11 | /// Gets or sets the Socket ID for a consuming Trigger 12 | /// 13 | string SocketId { get; set; } 14 | 15 | /// 16 | /// List of attributes that should be returned for each unique channel triggered to. 17 | /// 18 | List Info { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PusherServer/ITriggerResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PusherServer 4 | { 5 | /// 6 | /// Interface for Trigger Request Results 7 | /// 8 | public interface ITriggerResult: IRequestResult 9 | { 10 | /// 11 | /// Gets the Event IDs related to this Trigger Event 12 | /// 13 | IDictionary EventIds { get; } 14 | 15 | /// 16 | /// If requested via trigger options, returns channel attributes for each channel in Trigger Event request 17 | /// 18 | IDictionary ChannelAttributes { get; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PusherServer/IUserAuthenticationResponse.cs: -------------------------------------------------------------------------------- 1 | namespace PusherServer 2 | { 3 | /// 4 | /// Interface for Authenticaton Data 5 | /// 6 | public interface IUserAuthenticationResponse 7 | { 8 | /// 9 | /// Gets the Authetication String 10 | /// 11 | string auth { get; } 12 | 13 | /// 14 | /// Double encoded JSON containing user information. 15 | /// 16 | string user_data { get; } 17 | 18 | /// 19 | /// Returns a Json representation of the authentication data. 20 | /// 21 | /// 22 | string ToJson(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /PusherServer/IWebHook.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace PusherServer 5 | { 6 | /// 7 | /// Interface for Web Hooks 8 | /// 9 | public interface IWebHook 10 | { 11 | /// 12 | /// Indicates if the WebHook has validated. 13 | /// 14 | bool IsValid 15 | { 16 | get; 17 | } 18 | 19 | /// 20 | /// The Events in the WebHook 21 | /// 22 | Dictionary[] Events 23 | { 24 | get; 25 | } 26 | 27 | 28 | /// 29 | /// The timestamp of the WebHook 30 | /// 31 | DateTime Time 32 | { 33 | get; 34 | } 35 | 36 | /// 37 | /// An array of validation errors. If is true then the array 38 | /// will have no elements. 39 | /// 40 | string[] ValidationErrors 41 | { 42 | get; 43 | } 44 | 45 | } 46 | } -------------------------------------------------------------------------------- /PusherServer/PresenceChannelData.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace PusherServer 3 | { 4 | /// 5 | /// Information about a user who is subscribing to a presence channel. 6 | /// 7 | public class PresenceChannelData 8 | { 9 | /// 10 | /// A unique user identifier for the user witin the application. 11 | /// 12 | /// 13 | /// Pusher uses this to uniquely identify a user. So, if multiple users are given the same user_id 14 | /// the second of these users will be ignored and won't be represented on the presence channel. 15 | /// 16 | public string user_id { get; set; } 17 | 18 | /// 19 | /// Arbitrary additional information about the user. 20 | /// 21 | public object user_info { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /PusherServer/Properties/AssemblyInfo.Signed.cs: -------------------------------------------------------------------------------- 1 | [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("PusherServer.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100456864dbf1cbca1cfaca7dc1af4103e2fc957a7db4e525dd577054d01d7974a75a201a3c4856a513603ee24d893cfebb17199a2c9cd1a677d86f07ee612e21aff7c5c4f507872d343c93875a353b76f8c1d0b937cc563f0e361580940bb7a759739be3dce880f62cff52e36698efc38895b7cde9bc984c95b443075dbc8cf7dd")] 2 | -------------------------------------------------------------------------------- /PusherServer/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("PusherServer.Tests")] -------------------------------------------------------------------------------- /PusherServer/Properties/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pusher/pusher-http-dotnet/363dcf0bf68a486daae4674f8702f5f9eafb1e35/PusherServer/Properties/icon-128.png -------------------------------------------------------------------------------- /PusherServer/PusherOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using PusherServer.RestfulClient; 4 | using PusherServer.Util; 5 | 6 | namespace PusherServer 7 | { 8 | /// 9 | /// Options to be set on the Pusher instance. 10 | /// 11 | public class PusherOptions : IPusherOptions 12 | { 13 | /// 14 | /// The default Rest API Host for contacting the Pusher server, it does not contain a cluster name 15 | /// 16 | public const string DEFAULT_REST_API_HOST = "api.pusherapp.com"; 17 | 18 | /// 19 | /// The default batch event data size limit in bytes. 20 | /// 21 | public const int DEFAULT_BATCH_EVENT_DATA_SIZE_LIMIT = 10 * 1024; 22 | 23 | private const int DEFAULT_HTTPS_PORT = 443; 24 | private const int DEFAULT_HTTP_PORT = 80; 25 | 26 | IPusherRestClient _pusherClient; 27 | bool _encrypted; 28 | bool _portModified; 29 | bool _hostSet; 30 | int _port = DEFAULT_HTTP_PORT; 31 | string _hostName; 32 | string _cluster; 33 | ISerializeObjectsToJson _jsonSerializer; 34 | IDeserializeJsonStrings _jsonDeserializer; 35 | 36 | /// 37 | public bool Encrypted 38 | { 39 | get 40 | { 41 | return _encrypted; 42 | } 43 | set 44 | { 45 | _encrypted = value; 46 | if (_encrypted && _portModified == false) 47 | { 48 | _port = DEFAULT_HTTPS_PORT; 49 | } 50 | } 51 | } 52 | 53 | /// 54 | public int Port 55 | { 56 | get 57 | { 58 | return _port; 59 | } 60 | set 61 | { 62 | _port = value; 63 | _portModified = true; 64 | } 65 | } 66 | 67 | /// 68 | public string Cluster 69 | { 70 | get 71 | { 72 | return _cluster; 73 | } 74 | set 75 | { 76 | if (_hostSet == false) { 77 | _cluster = value; 78 | _hostName = "api-"+_cluster+".pusher.com"; 79 | } 80 | } 81 | } 82 | 83 | /// 84 | public IPusherRestClient RestClient 85 | { 86 | get 87 | { 88 | if (_pusherClient == null) 89 | { 90 | _pusherClient = new PusherRestClient(GetBaseUrl(), Pusher.LIBRARY_NAME, Pusher.VERSION) 91 | { 92 | Timeout = RestClientTimeout, 93 | }; 94 | } 95 | 96 | return _pusherClient; 97 | } 98 | set { _pusherClient = value; } 99 | } 100 | 101 | /// 102 | public TimeSpan RestClientTimeout { get; set; } = TimeSpan.FromSeconds(30); 103 | 104 | /// 105 | public string HostName 106 | { 107 | get 108 | { 109 | return _hostName ?? DEFAULT_REST_API_HOST; 110 | } 111 | set 112 | { 113 | if (Regex.IsMatch(value, "^.*://")) 114 | { 115 | string msg = string.Format("The scheme should not be present in the host value: {0}", value); 116 | throw new FormatException(msg); 117 | } 118 | 119 | _hostSet = true; 120 | _cluster = null; 121 | _hostName = value; 122 | } 123 | } 124 | 125 | /// 126 | public ISerializeObjectsToJson JsonSerializer 127 | { 128 | get 129 | { 130 | if (_jsonSerializer == null) 131 | { 132 | _jsonSerializer = new DefaultSerializer(); 133 | } 134 | 135 | return _jsonSerializer; 136 | 137 | } 138 | set { _jsonSerializer = value; } 139 | } 140 | 141 | /// 142 | public IDeserializeJsonStrings JsonDeserializer 143 | { 144 | get 145 | { 146 | if (_jsonDeserializer == null) 147 | { 148 | _jsonDeserializer = new DefaultDeserializer(); 149 | } 150 | 151 | return _jsonDeserializer; 152 | } 153 | set 154 | { 155 | _jsonDeserializer = value; 156 | } 157 | } 158 | 159 | /// 160 | public int? BatchEventDataSizeLimit { get; set; } 161 | 162 | /// 163 | public ITraceLogger TraceLogger { get; set; } 164 | 165 | /// 166 | public byte[] EncryptionMasterKey { get; set; } 167 | 168 | /// 169 | public Uri GetBaseUrl() 170 | { 171 | string baseUrl = (Encrypted ? "https" : "http") + "://" + HostName + GetPort(); 172 | 173 | return new Uri(baseUrl); 174 | } 175 | 176 | private string GetPort() 177 | { 178 | var port = string.Empty; 179 | 180 | if (Port != DEFAULT_HTTP_PORT) 181 | { 182 | port += (":" + Port); 183 | } 184 | 185 | return port; 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /PusherServer/PusherServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | net45;net472;netstandard1.3;netstandard2.0 6 | true 7 | 2.0.0 8 | PusherServer 9 | PusherServer 10 | false 11 | 12 | 13 | 14 | ..\PusherServer.snk 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /PusherServer/RawBodySerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PusherServer 4 | { 5 | /// 6 | /// An implementation of the that passes through the raw string. 7 | /// 8 | public class RawBodySerializer : ISerializeObjectsToJson 9 | { 10 | /// 11 | /// Presumes we are getting a string as the body, and passes it through. 12 | /// 13 | /// The string body to pass through. 14 | /// The body passed in. 15 | /// Thrown if the body provided is not a string. 16 | public string Serialize(object body) 17 | { 18 | if (body == null) 19 | { 20 | return string.Empty; 21 | } 22 | 23 | var bodyAsString = body as string; 24 | if(bodyAsString == null) 25 | { 26 | throw new ArgumentException("The RawBodySerializer only supports strings for messages. The body type was: " + body.GetType().Name, "body"); 27 | } 28 | 29 | return bodyAsString; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /PusherServer/RequestResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | 5 | namespace PusherServer 6 | { 7 | /// 8 | /// Abstract base class for results coming back from request to the Pusher servers 9 | /// 10 | public abstract class RequestResult : IRequestResult 11 | { 12 | /// 13 | /// Constructor to constract the abstract base class for classes derived from RequestResults 14 | /// 15 | /// 16 | /// 17 | protected RequestResult(HttpResponseMessage response, string originalContent) 18 | { 19 | if (response == null) 20 | { 21 | throw new ArgumentNullException(nameof(response)); 22 | } 23 | 24 | Body = originalContent; 25 | StatusCode = response.StatusCode; 26 | 27 | Response = response; 28 | } 29 | 30 | /// 31 | /// Gets the Status Code returned in the wrapped Rest Response 32 | /// 33 | public HttpStatusCode StatusCode { get; protected set; } 34 | 35 | /// 36 | /// Gets the Body returned in the wrapped Rest Response 37 | /// 38 | public string Body { get; protected set; } = null; 39 | 40 | /// 41 | /// Gets the original content that was returned in the response, if the response returned was Bad 42 | /// 43 | public HttpContent OriginalContent => Response?.Content; 44 | 45 | /// 46 | /// Gets the original response from the rest service 47 | /// 48 | public HttpResponseMessage Response { get; private set; } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /PusherServer/RestfulClient/AuthenticatedRequestFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | 5 | namespace PusherServer.RestfulClient 6 | { 7 | /// 8 | /// Factory that creates authenticated requests to send to the Pusher API 9 | /// 10 | public class AuthenticatedRequestFactory : IAuthenticatedRequestFactory 11 | { 12 | private readonly string _appKey; 13 | private readonly string _appId; 14 | private readonly string _appSecret; 15 | 16 | /// 17 | /// Constructs a new Autheticated Request Factory 18 | /// 19 | /// Your app Key for the Pusher API 20 | /// Your app Id for the Pusher API 21 | /// Your app Secret for the Pusher API 22 | public AuthenticatedRequestFactory(string appKey, string appId, string appSecret) 23 | { 24 | _appKey = appKey; 25 | _appId = appId; 26 | _appSecret = appSecret; 27 | } 28 | 29 | /// 30 | public IPusherRestRequest Build(PusherMethod requestType, string resource, object requestParameters = null, object requestBody = null) 31 | { 32 | SortedDictionary queryParams = GetQueryString(requestParameters, requestBody); 33 | 34 | string queryString = string.Empty; 35 | foreach (KeyValuePair parameter in queryParams) 36 | { 37 | queryString += parameter.Key + "=" + parameter.Value + "&"; 38 | } 39 | queryString = queryString.TrimEnd('&'); 40 | 41 | string path = $"/apps/{_appId}/{resource.TrimStart('/')}"; 42 | 43 | string authToSign = String.Format( 44 | Enum.GetName(requestType.GetType(), requestType) + 45 | "\n{0}\n{1}", 46 | path, 47 | queryString); 48 | 49 | string authSignature = CryptoHelper.GetHmac256(_appSecret, authToSign); 50 | 51 | string requestUrl = $"{path}?auth_signature={authSignature}&{queryString}"; 52 | 53 | IPusherRestRequest request = new PusherRestRequest(requestUrl) 54 | { 55 | Method = requestType, 56 | Body = requestBody, 57 | }; 58 | 59 | return request; 60 | } 61 | 62 | private SortedDictionary GetQueryString(object requestParameters, object requestBody) 63 | { 64 | SortedDictionary parameters = GetStringBuilderfromSourceObject(requestParameters); 65 | 66 | int timeNow = (int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds; 67 | 68 | parameters.Add("auth_key", _appKey); 69 | parameters.Add("auth_timestamp", timeNow.ToString()); 70 | parameters.Add("auth_version", "1.0"); 71 | 72 | if (requestBody != null) 73 | { 74 | var bodyDataJson = DefaultSerializer.Default.Serialize(requestBody); 75 | var bodyMd5 = CryptoHelper.GetMd5Hash(bodyDataJson); 76 | parameters.Add("body_md5", bodyMd5); 77 | } 78 | 79 | return parameters; 80 | } 81 | 82 | private static SortedDictionary GetStringBuilderfromSourceObject(object sourceObject) 83 | { 84 | SortedDictionary parameters = new SortedDictionary(); 85 | 86 | if (sourceObject != null) 87 | { 88 | Type objType = sourceObject.GetType(); 89 | IList propertyInfos = new List(objType.GetTypeInfo().DeclaredProperties); 90 | 91 | foreach (PropertyInfo propertyInfo in propertyInfos) 92 | { 93 | parameters.Add(propertyInfo.Name, propertyInfo.GetValue(sourceObject, null).ToString()); 94 | } 95 | } 96 | 97 | return parameters; 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /PusherServer/RestfulClient/IAuthenticatedRequestFactory.cs: -------------------------------------------------------------------------------- 1 | namespace PusherServer.RestfulClient 2 | { 3 | /// 4 | /// The contract for the factory that creates authenticated requests to send to the Pusher API 5 | /// 6 | public interface IAuthenticatedRequestFactory 7 | { 8 | /// 9 | /// Builds a new authenticated request to send to Pusher 10 | /// 11 | /// What type of REST call is to be made 12 | /// The resource path for the REST call 13 | /// (Optional) Any parameters that need to be included in the call 14 | /// (Optional) The body to be sent with the request 15 | /// A constructed REST request 16 | IPusherRestRequest Build(PusherMethod requestType, string resource, object requestParameters = null, object requestBody = null); 17 | } 18 | } -------------------------------------------------------------------------------- /PusherServer/RestfulClient/IPusherRestClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace PusherServer.RestfulClient 5 | { 6 | /// 7 | /// Contract for a client for the Pusher REST requests 8 | /// 9 | public interface IPusherRestClient 10 | { 11 | /// 12 | /// Execute a REST GET request to the Pusher API asynchronously 13 | /// 14 | /// The request to execute 15 | /// The response received from Pusher 16 | Task> ExecuteGetAsync(IPusherRestRequest pusherRestRequest); 17 | 18 | /// 19 | /// Execute a REST POST request to the Pusher API asynchronously 20 | /// 21 | /// The request to execute 22 | /// The response received from Pusher 23 | Task ExecutePostAsync(IPusherRestRequest pusherRestRequest); 24 | 25 | /// 26 | /// Gets the Base Url this client is using 27 | /// 28 | Uri BaseUrl { get; } 29 | 30 | /// 31 | /// Gets or sets the Pusher rest client timeout. The default timeout is 30 seconds. 32 | /// 33 | TimeSpan Timeout { get; set; } 34 | } 35 | } -------------------------------------------------------------------------------- /PusherServer/RestfulClient/IPusherRestRequest.cs: -------------------------------------------------------------------------------- 1 | namespace PusherServer.RestfulClient 2 | { 3 | /// 4 | /// The contract for a REST request to be made to the Pusher API 5 | /// 6 | public interface IPusherRestRequest 7 | { 8 | /// 9 | /// Gets or sets the type of RESTful call to make 10 | /// 11 | PusherMethod Method { get; set; } 12 | 13 | /// 14 | /// Gets the Resource Uri for this request 15 | /// 16 | string ResourceUri { get; } 17 | 18 | /// 19 | /// Gets or sets the content that will be sent with the request 20 | /// 21 | object Body { get; set; } 22 | 23 | /// 24 | /// Gets the current body as a Json String 25 | /// 26 | /// 27 | string GetContentAsJsonString(); 28 | } 29 | } -------------------------------------------------------------------------------- /PusherServer/RestfulClient/PusherMethod.cs: -------------------------------------------------------------------------------- 1 | namespace PusherServer.RestfulClient 2 | { 3 | /// 4 | /// Enum used to determine what kind of REST request to make 5 | /// 6 | public enum PusherMethod 7 | { 8 | /// 9 | /// Make a GET request 10 | /// 11 | GET, 12 | /// 13 | /// Make a POST request 14 | /// 15 | POST 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /PusherServer/RestfulClient/PusherRestClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Net.Http.Headers; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace PusherServer.RestfulClient 8 | { 9 | /// 10 | /// A client for the Pusher REST requests 11 | /// 12 | public class PusherRestClient : IPusherRestClient 13 | { 14 | private readonly string _libraryName; 15 | private readonly string _version; 16 | private readonly HttpClient _httpClient; 17 | 18 | /// 19 | /// Constructs a new instance of the PusherRestClient 20 | /// 21 | /// The base address of the Pusher API as a URI formatted string 22 | /// 23 | /// 24 | public PusherRestClient(string baseAddress, string libraryName, Version version) : this(new Uri(baseAddress), libraryName, version) 25 | {} 26 | 27 | /// 28 | /// Constructs a new instance of the PusherRestClient with a supplied HttpClient 29 | /// 30 | /// The base address of the Pusher API 31 | /// The name of the Pusher Library 32 | /// The version of the Pusher library 33 | public PusherRestClient(Uri baseAddress, string libraryName, Version version):this(CreateDefaultHttpClient(baseAddress), libraryName, version) 34 | {} 35 | 36 | private static HttpClient CreateDefaultHttpClient(Uri baseAddress) 37 | { 38 | return new HttpClient 39 | { 40 | BaseAddress = baseAddress, 41 | Timeout = TimeSpan.FromSeconds(30), 42 | }; 43 | } 44 | 45 | /// 46 | /// Constructs a new instance of the PusherRestClient with a supplied HttpClient 47 | /// 48 | /// An externally configured HttpClient 49 | /// The name of the Pusher Library 50 | /// The version of the Pusher library 51 | public PusherRestClient(HttpClient httpClient, string libraryName, Version version) 52 | { 53 | _httpClient = httpClient; 54 | _libraryName = libraryName; 55 | _version = version.ToString(3); 56 | _httpClient.DefaultRequestHeaders.Clear(); 57 | _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 58 | _httpClient.DefaultRequestHeaders.Add("Pusher-Library-Name", _libraryName); 59 | _httpClient.DefaultRequestHeaders.Add("Pusher-Library-Version", _version); 60 | } 61 | 62 | /// 63 | public Uri BaseUrl { 64 | get { return _httpClient.BaseAddress; } 65 | } 66 | 67 | /// 68 | public TimeSpan Timeout 69 | { 70 | get 71 | { 72 | return _httpClient.Timeout; 73 | } 74 | 75 | set 76 | { 77 | _httpClient.Timeout = value; 78 | } 79 | } 80 | 81 | /// 82 | public async Task> ExecuteGetAsync(IPusherRestRequest pusherRestRequest) 83 | { 84 | GetResult result = null; 85 | if (pusherRestRequest.Method == PusherMethod.GET) 86 | { 87 | var response = await _httpClient.GetAsync(pusherRestRequest.ResourceUri).ConfigureAwait(false); 88 | var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 89 | 90 | result = new GetResult(response, responseContent); 91 | } 92 | 93 | return result; 94 | } 95 | 96 | /// 97 | public async Task ExecutePostAsync(IPusherRestRequest pusherRestRequest) 98 | { 99 | TriggerResult result = null; 100 | if (pusherRestRequest.Method == PusherMethod.POST) 101 | { 102 | var content = new StringContent(pusherRestRequest.GetContentAsJsonString(), Encoding.UTF8, "application/json"); 103 | 104 | var response = await _httpClient.PostAsync(pusherRestRequest.ResourceUri, content).ConfigureAwait(false); 105 | var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 106 | 107 | result = new TriggerResult(response, responseContent); 108 | } 109 | 110 | return result; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /PusherServer/RestfulClient/PusherRestRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace PusherServer.RestfulClient 6 | { 7 | /// 8 | /// A REST request to be made to the Pusher API 9 | /// 10 | public class PusherRestRequest : IPusherRestRequest 11 | { 12 | /// 13 | /// Creates a new REST request to make back to Pusher HQ 14 | /// 15 | /// The URI to call 16 | public PusherRestRequest(string resourceUri) 17 | { 18 | if (string.IsNullOrWhiteSpace(resourceUri)) 19 | throw new ArgumentNullException(nameof(resourceUri), "The resource URI must be a populated string"); 20 | 21 | ResourceUri = resourceUri; 22 | } 23 | 24 | /// 25 | public PusherMethod Method { get; set; } 26 | 27 | /// 28 | public string ResourceUri { get; } 29 | 30 | /// 31 | public object Body { get; set; } 32 | 33 | /// 34 | public string GetContentAsJsonString() 35 | { 36 | return Body != null ? DefaultSerializer.Default.Serialize(Body) : null; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /PusherServer/TriggerBody.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PusherServer 4 | { 5 | /// 6 | /// Represents the payload to be sent when triggering events 7 | /// 8 | internal class TriggerBody 9 | { 10 | /// 11 | /// The name of the event 12 | /// 13 | public string name { get; set; } 14 | 15 | /// 16 | /// The event data 17 | /// 18 | public string data { get; set; } 19 | 20 | /// 21 | /// The channels the event should be triggered on. 22 | /// 23 | public string[] channels { get; set; } 24 | 25 | /// 26 | /// The id of a socket to be excluded from receiving the event. 27 | /// 28 | public string socket_id { get; set; } 29 | 30 | /// 31 | /// A comma-separated list of attributes that should be returned for each unique channel triggered to. 32 | /// 33 | public string info { get; set; } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /PusherServer/TriggerOptions.cs: -------------------------------------------------------------------------------- 1 |  2 | using System.Collections.Generic; 3 | 4 | namespace PusherServer 5 | { 6 | /// 7 | /// Represents the Options that can be used by A Trigger 8 | /// 9 | public class TriggerOptions: ITriggerOptions 10 | { 11 | /// 12 | /// Gets or sets the Socket ID for the consuming Trigger 13 | /// 14 | public string SocketId { get; set; } 15 | 16 | /// 17 | /// List of attributes that should be returned for each unique channel triggered to. 18 | /// 19 | public List Info { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PusherServer/TriggerResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using Newtonsoft.Json; 5 | using PusherServer.Exceptions; 6 | using PusherServer.Util; 7 | 8 | namespace PusherServer 9 | { 10 | /// 11 | /// The response from a Trigger REST request 12 | /// 13 | public class TriggerResult : RequestResult, ITriggerResult 14 | { 15 | /// 16 | /// Constructs a new instance of a TriggerResult based upon a passed in Rest Response 17 | /// 18 | /// The Rest Response which will form the basis of this Trigger Result 19 | /// The response content as a string 20 | public TriggerResult(HttpResponseMessage response, string responseContent) : base(response, responseContent) 21 | { 22 | EventIdData eventIdData; 23 | 24 | try 25 | { 26 | eventIdData = JsonConvert.DeserializeObject(responseContent); 27 | } 28 | catch (Exception e) 29 | { 30 | string msg = $"The response body from the Pusher HTTP endpoint could not be parsed as JSON: {Environment.NewLine}{responseContent}"; 31 | throw new TriggerResponseException(msg, e); 32 | } 33 | 34 | EventIds = new ReadOnlyDictionary(eventIdData.event_ids); 35 | ChannelAttributes = new ReadOnlyDictionary(eventIdData.channels); 36 | } 37 | 38 | /// 39 | public IDictionary EventIds { get; } 40 | public IDictionary ChannelAttributes { get; } 41 | } 42 | } -------------------------------------------------------------------------------- /PusherServer/UserAuthenticationResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace PusherServer 5 | { 6 | [DataContract] 7 | class UserAuthenticationResponse: IUserAuthenticationResponse 8 | { 9 | private readonly string _appKey; 10 | private readonly string _appSecret; 11 | private readonly string _socketId; 12 | private readonly UserData _userData; 13 | 14 | public UserAuthenticationResponse(string appKey, string appSecret, string socketId, UserData userData) 15 | { 16 | ValidationHelper.ValidateSocketId(socketId); 17 | if (userData == null) 18 | { 19 | throw new ArgumentNullException(nameof(userData)); 20 | } 21 | 22 | _appKey = appKey; 23 | _appSecret = appSecret; 24 | _socketId = socketId; 25 | _userData = userData; 26 | } 27 | 28 | [DataMember(Name = "auth", IsRequired = true)] 29 | public string auth 30 | { 31 | get 32 | { 33 | var userDataJson = DefaultSerializer.Default.Serialize(_userData); 34 | var stringToSign = _socketId + "::user::" + userDataJson; 35 | 36 | return _appKey + ":" + CryptoHelper.GetHmac256(_appSecret, stringToSign); 37 | } 38 | } 39 | 40 | /// 41 | /// Double encoded JSON containing user information. 42 | /// 43 | [DataMember(Name = "user_data", IsRequired = false, EmitDefaultValue = false)] 44 | public string user_data 45 | { 46 | get 47 | { 48 | return DefaultSerializer.Default.Serialize(_userData); 49 | } 50 | } 51 | 52 | public string ToJson() 53 | { 54 | return ToString(); 55 | } 56 | 57 | public override string ToString() 58 | { 59 | return DefaultSerializer.Default.Serialize(this); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /PusherServer/UserData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PusherServer 4 | { 5 | /// 6 | /// Information about a user who is authenticating to Pusher. 7 | /// 8 | public class UserData 9 | { 10 | /// 11 | /// A unique user identifier for the user witin the application. 12 | /// 13 | /// 14 | /// Pusher uses this to uniquely identify a user. 15 | /// 16 | public string id { get; set; } 17 | 18 | /// 19 | /// A list of user ids representing the circle of interest for this user. 20 | /// 21 | public string[] watchlist { get; set; } 22 | 23 | /// 24 | /// Arbitrary additional information about the user. 25 | /// 26 | public object user_info { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /PusherServer/Util/DebugTraceLogger.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace PusherServer.Util 4 | { 5 | /// 6 | /// Traces debug messages using . 7 | /// 8 | public class DebugTraceLogger : ITraceLogger 9 | { 10 | /// 11 | public void Trace(string message) 12 | { 13 | Debug.WriteLine(message); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /PusherServer/Util/ITraceLogger.cs: -------------------------------------------------------------------------------- 1 | namespace PusherServer.Util 2 | { 3 | /// 4 | /// An interface for tracing debug messages. 5 | /// 6 | public interface ITraceLogger 7 | { 8 | /// 9 | /// Traces a debug message. 10 | /// 11 | /// The message to trace. 12 | void Trace(string message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /PusherServer/ValidationHelper.cs: -------------------------------------------------------------------------------- 1 | using PusherServer.Exceptions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace PusherServer 7 | { 8 | /// 9 | /// Helps validation of channel names and socket_id values. 10 | /// 11 | public static class ValidationHelper 12 | { 13 | /// 14 | /// A regular expression to check that a channel name is in a format allowed and accepted by Pusher. 15 | /// 16 | public static Regex CHANNEL_NAME_REGEX = new Regex(@"\A[a-zA-Z0-9_=@,.;\-]+\z", RegexOptions.Singleline); 17 | 18 | /// 19 | /// The maximum length of a channel name allowed by Pusher. 20 | /// 21 | public static int CHANNEL_NAME_MAX_LENGTH = 164; 22 | 23 | /// 24 | /// A regular expression to check that a socket_id is in a format allowed and accepted by Pusher. 25 | /// 26 | public static Regex SOCKET_ID_REGEX = new Regex(@"\A\d+\.\d+\z", RegexOptions.Singleline); 27 | 28 | /// 29 | /// The maximum event batch size accepted by Pusher 30 | /// 31 | public static int MAX_BATCH_SIZE = 10; 32 | 33 | /// 34 | /// The expected encryption master key length. 35 | /// 36 | public static int ENCRYPTION_MASTER_KEY_LENGTH = 32; 37 | 38 | /// 39 | /// Validate a value. 40 | /// 41 | /// The value to be checked. 42 | /// If the is not in the allowed format. 43 | internal static void ValidateSocketId(string socketId) 44 | { 45 | if (socketId != null && SOCKET_ID_REGEX.IsMatch(socketId) == false) 46 | { 47 | throw new SocketIdFormatException(actualValue: socketId); 48 | } 49 | } 50 | 51 | /// 52 | /// Validate a . 53 | /// 54 | /// The channel name to be checked. 55 | /// If the length of the is longer than expected. 56 | /// If the is not in the allowed format. 57 | internal static void ValidateChannelName(string channelName) 58 | { 59 | if (CHANNEL_NAME_REGEX.IsMatch(channelName) == false) 60 | { 61 | throw new ChannelNameFormatException(actualValue: channelName); 62 | } 63 | 64 | if (channelName.Length > CHANNEL_NAME_MAX_LENGTH) 65 | { 66 | throw new ChannelNameLengthExceededException(nameof(channelName), channelName.Length); 67 | } 68 | } 69 | 70 | /// 71 | /// Validate an array of channel names 72 | /// 73 | /// The array of channel names 74 | /// If the length of any channel name is longer than expected. 75 | /// If any channel names are not in the allowed format. 76 | internal static void ValidateChannelNames(IEnumerable channelNames) 77 | { 78 | int encryptedChannelCount = 0; 79 | int channelCount = 0; 80 | foreach(string name in channelNames) 81 | { 82 | ValidateChannelName(name); 83 | if (Pusher.IsPrivateEncryptedChannel(name)) 84 | { 85 | encryptedChannelCount++; 86 | } 87 | 88 | channelCount++; 89 | } 90 | 91 | if (channelCount > 1 && encryptedChannelCount >= 1) 92 | { 93 | throw new InvalidOperationException("You cannot trigger to multiple channels when using encrypted channels."); 94 | } 95 | } 96 | 97 | /// 98 | /// Validates a batch of events. 99 | /// 100 | /// The batch of events to validate. 101 | /// If the length of any channel name is longer than expected. 102 | /// If any channel names are not in the allowed format. 103 | /// If the size of the batch is greater than 10. 104 | internal static void ValidateBatchEvents(Event[] events) 105 | { 106 | if (events.Length > MAX_BATCH_SIZE) 107 | { 108 | throw new EventBatchSizeExceededException(nameof(events), events.Length); 109 | } 110 | 111 | foreach (Event e in events) 112 | { 113 | ValidateChannelName(e.Channel); 114 | ValidateSocketId(e.SocketId); 115 | } 116 | } 117 | 118 | /// 119 | /// Validates the size of the data field for a batch event. 120 | /// 121 | /// The data field to validate. 122 | /// The name of a channel associated with event data. 123 | /// The name of the event. 124 | /// The current . 125 | /// If the size of is greater than the expected. 126 | /// Note: the size of the data field is only validated if the IPusherOption.BatchEventDataSizeLimit is specified. 127 | internal static void ValidateBatchEventData(string data, string channelName, string eventName, IPusherOptions options) 128 | { 129 | if (options != null && options.BatchEventDataSizeLimit.HasValue) 130 | { 131 | if (data != null && data.Length > options.BatchEventDataSizeLimit.Value) 132 | { 133 | throw new EventDataSizeExceededException(options.BatchEventDataSizeLimit.Value, data.Length) 134 | { 135 | ChannelName = channelName, 136 | EventName = eventName, 137 | }; 138 | } 139 | } 140 | } 141 | 142 | /// 143 | /// Validate a value. 144 | /// 145 | /// The encryption master key to be checked. 146 | /// If the is not the specified length. 147 | internal static void ValidateEncryptionMasterKey(byte[] encryptionMasterKey) 148 | { 149 | int length = encryptionMasterKey != null ? encryptionMasterKey.Length : 0; 150 | if (length != ENCRYPTION_MASTER_KEY_LENGTH) 151 | { 152 | throw new EncryptionMasterKeyException(nameof(encryptionMasterKey), length); 153 | } 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /PusherServer/WebHook.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace PusherServer 6 | { 7 | internal class WebHook: IWebHook 8 | { 9 | private readonly WebHookData _webHookData; 10 | private readonly List _validationErrors; 11 | private IDeserializeJsonStrings _jsonDeserializer; 12 | 13 | internal WebHook(string secret, string signature, string body) 14 | { 15 | if(string.IsNullOrEmpty(secret)) 16 | { 17 | throw new ArgumentException("A secret must be provided", "secret"); 18 | } 19 | 20 | this._validationErrors = new List(); 21 | 22 | this._webHookData = ValidateWebHook(secret, signature, body); 23 | } 24 | 25 | private WebHookData ValidateWebHook(string secret, string signature, string body) 26 | { 27 | WebHookData parsedWebHookData = null; 28 | 29 | var signatureNullOrEmpth = string.IsNullOrEmpty(signature); 30 | if(signatureNullOrEmpth == true) { 31 | this._validationErrors.Add("The supplied signature to check was null or empty. A signature to check must be provided."); 32 | } 33 | 34 | if (string.IsNullOrEmpty(body) == true) 35 | { 36 | this._validationErrors.Add("The supplied body to check was null or empty. A body to check must be provided."); 37 | } 38 | else 39 | { 40 | try 41 | { 42 | parsedWebHookData = JsonDeserializer.Deserialize(body); 43 | } 44 | catch (Exception e) 45 | { 46 | var validationError = string.Format("Exception occurred parsing the body as JSON: {0}{1}", Environment.NewLine, e.StackTrace); 47 | this._validationErrors.Add(validationError); 48 | } 49 | } 50 | 51 | if (parsedWebHookData != null) 52 | { 53 | var expectedSignature = CryptoHelper.GetHmac256(secret, body); 54 | var signatureIsValid = (signature == expectedSignature); 55 | if (signatureIsValid == false) 56 | { 57 | this._validationErrors.Add( 58 | string.Format("The signature did not validate. Expected {0}. Got {1}", signature, expectedSignature) 59 | ); 60 | } 61 | } 62 | return parsedWebHookData; 63 | } 64 | 65 | public bool IsValid 66 | { 67 | get 68 | { 69 | return (this.ValidationErrors.Length == 0); 70 | } 71 | } 72 | 73 | public Dictionary[] Events 74 | { 75 | get 76 | { 77 | return this._webHookData.events; 78 | } 79 | } 80 | 81 | public DateTime Time 82 | { 83 | get 84 | { 85 | return this._webHookData.Time; 86 | } 87 | } 88 | 89 | public string[] ValidationErrors 90 | { 91 | get 92 | { 93 | return this._validationErrors.ToArray(); 94 | } 95 | } 96 | 97 | /// 98 | /// Gets or sets the JSON Deserializer to use 99 | /// 100 | public IDeserializeJsonStrings JsonDeserializer 101 | { 102 | get 103 | { 104 | if (_jsonDeserializer == null) 105 | { 106 | _jsonDeserializer = new DefaultDeserializer(); 107 | } 108 | 109 | return _jsonDeserializer; 110 | } 111 | set { _jsonDeserializer = value; } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /PusherServer/WebHookData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace PusherServer 5 | { 6 | /// 7 | /// Represents the Data payload of a Web Hook 8 | /// 9 | public class WebHookData 10 | { 11 | private DateTime _time; 12 | 13 | /// 14 | /// Gets or sets the Time the Web Hook was created in Milliseconds 15 | /// 16 | public string time_ms 17 | { 18 | get 19 | { 20 | // This should not be used. 21 | return GetUnixTimestampMillis(this.Time).ToString(); 22 | } 23 | set 24 | { 25 | long unixTimeStamp = long.Parse(value); 26 | _time = DateTimeFromUnixTimestampMillis(unixTimeStamp); 27 | } 28 | } 29 | /// 30 | /// Gets or sets the Events being triggered 31 | /// 32 | public Dictionary[] events { get; set; } 33 | 34 | /// 35 | /// Gets the Time the Web Hook was created 36 | /// 37 | public DateTime Time 38 | { 39 | get 40 | { 41 | return this._time; 42 | } 43 | } 44 | 45 | private static readonly DateTime UnixEpoch = 46 | new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); 47 | 48 | private static long GetUnixTimestampMillis(DateTime dateTime) 49 | { 50 | return (long)(dateTime - UnixEpoch).TotalMilliseconds; 51 | } 52 | 53 | private static DateTime DateTimeFromUnixTimestampMillis(long millis) 54 | { 55 | return UnixEpoch.AddMilliseconds(millis); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /PusherServer/WebHookEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace PusherServer 7 | { 8 | /// 9 | /// A Web Hook Event 10 | /// 11 | public class WebHookEvent 12 | { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Root.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Pusher 6 | Pusher 7 | Pusher .NET library for interacting with the HTTP API 8 | Copyright © 2021 9 | false 10 | https://github.com/pusher/pusher-http-dotnet 11 | See https://github.com/pusher/pusher-http-dotnet/blob/master/CHANGELOG.md for full details 12 | pusher channels realtime websocket 13 | MIT 14 | icon-128.png 15 | 5.0.0 16 | 5.0.0.0 17 | 5.0.0.0 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /StrongName/GeneratePusherKey.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "stop"; 2 | $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition; 3 | Push-Location $scriptDir; 4 | try { 5 | $keyName = "PusherServer"; 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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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\..\PusherServer.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 | } -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # exit if any command fails 4 | set -e 5 | 6 | dotnet restore ./PusherServer.Core/PusherServer.Core.csproj 7 | 8 | # Instead, run directly with mono for the full .net version 9 | dotnet build ./PusherServer.Core/PusherServer.Core.csproj -c Release -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pusher-dotnet-server.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31005.135 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Files", "Files", "{13FCFB53-1898-4AE9-B229-F917B8F52DB2}" 7 | ProjectSection(SolutionItems) = preProject 8 | .gitattributes = .gitattributes 9 | .gitignore = .gitignore 10 | AppConfig.sample.json = AppConfig.sample.json 11 | CHANGELOG.md = CHANGELOG.md 12 | LICENSE.txt = LICENSE.txt 13 | NetFxOnLinux.props = NetFxOnLinux.props 14 | pull_request_template.md = pull_request_template.md 15 | README.md = README.md 16 | Root.Build.props = Root.Build.props 17 | EndProjectSection 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PusherServer.Tests", "PusherServer.Tests\PusherServer.Tests.csproj", "{68D41211-774A-4CF4-846E-0C38FD968D7C}" 20 | EndProject 21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PusherServer", "PusherServer\PusherServer.csproj", "{1B404714-5F81-4F3E-836E-106D88972A59}" 22 | EndProject 23 | Global 24 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 25 | Debug|Any CPU = Debug|Any CPU 26 | Release|Any CPU = Release|Any CPU 27 | EndGlobalSection 28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 29 | {68D41211-774A-4CF4-846E-0C38FD968D7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {68D41211-774A-4CF4-846E-0C38FD968D7C}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {68D41211-774A-4CF4-846E-0C38FD968D7C}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {68D41211-774A-4CF4-846E-0C38FD968D7C}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {1B404714-5F81-4F3E-836E-106D88972A59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {1B404714-5F81-4F3E-836E-106D88972A59}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {1B404714-5F81-4F3E-836E-106D88972A59}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {1B404714-5F81-4F3E-836E-106D88972A59}.Release|Any CPU.Build.0 = Release|Any CPU 37 | EndGlobalSection 38 | GlobalSection(SolutionProperties) = preSolution 39 | HideSolutionNode = FALSE 40 | EndGlobalSection 41 | GlobalSection(ExtensibilityGlobals) = postSolution 42 | SolutionGuid = {1C25E22B-A520-4475-8E99-CFAD0D934C6E} 43 | EndGlobalSection 44 | GlobalSection(TestCaseManagementSettings) = postSolution 45 | CategoryFile = PusherRESTDotNet.vsmdi 46 | EndGlobalSection 47 | EndGlobal 48 | --------------------------------------------------------------------------------