├── ConnectToReverseProxyDiagram.png ├── GameServer.ReverseProxy ├── Program.cs ├── .dockerignore ├── GameServer.ReverseProxy.csproj ├── appsettings.json ├── Dockerfile ├── Properties │ └── launchSettings.json ├── GameServer.ReverseProxy.sln ├── ServerEndpointFactory.cs ├── .gitignore └── Startup.cs ├── LICENSE.txt └── README.md /ConnectToReverseProxyDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlayFab/MultiplayerServerSecureWebsocket/HEAD/ConnectToReverseProxyDiagram.png -------------------------------------------------------------------------------- /GameServer.ReverseProxy/Program.cs: -------------------------------------------------------------------------------- 1 | using GameServer.ReverseProxy; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Hosting; 4 | 5 | Host.CreateDefaultBuilder(args) 6 | .ConfigureWebHostDefaults(webBuilder => 7 | { 8 | webBuilder.UseStartup(); 9 | }).Build().Run(); -------------------------------------------------------------------------------- /GameServer.ReverseProxy/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /GameServer.ReverseProxy/GameServer.ReverseProxy.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | 9fe441dc-98aa-4056-880c-0b24a69dd45f 6 | Linux 7 | . 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /GameServer.ReverseProxy/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "PlayFab": { 11 | "TitleId": "", 12 | "SecretKey": "", 13 | }, 14 | "ReverseProxy": { 15 | "Clusters": { 16 | "allClusterProps": { 17 | "SessionAffinity": { 18 | "Enabled": true, 19 | "Policy": "Cookie", 20 | "FailurePolicy": "Redistribute", 21 | "AffinityKeyName": "ARRAffinity" 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /GameServer.ReverseProxy/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | EXPOSE 443 7 | 8 | FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build 9 | WORKDIR /src 10 | COPY ["GameServer.ReverseProxy.csproj", "."] 11 | RUN dotnet restore "./GameServer.ReverseProxy.csproj" 12 | COPY . . 13 | WORKDIR "/src/." 14 | RUN dotnet build "GameServer.ReverseProxy.csproj" -c Release -o /app/build 15 | 16 | FROM build AS publish 17 | RUN dotnet publish "GameServer.ReverseProxy.csproj" -c Release -o /app/publish 18 | 19 | FROM base AS final 20 | WORKDIR /app 21 | COPY --from=publish /app/publish . 22 | ENTRYPOINT ["dotnet", "GameServer.ReverseProxy.dll"] 23 | -------------------------------------------------------------------------------- /GameServer.ReverseProxy/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:13591", 7 | "sslPort": 44319 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "MultiplayerServerSecureWebsocket": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "dotnetRunMessages": "true", 25 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 26 | }, 27 | "Docker": { 28 | "commandName": "Docker", 29 | "launchBrowser": true, 30 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", 31 | "publishAllPorts": true, 32 | "useSSL": true 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Microsoft Corporation 4 | 5 | All rights reserved. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. -------------------------------------------------------------------------------- /GameServer.ReverseProxy/GameServer.ReverseProxy.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31702.278 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameServer.ReverseProxy", "GameServer.ReverseProxy.csproj", "{968A6A28-A62D-430A-AEA9-4C3926B4E061}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {968A6A28-A62D-430A-AEA9-4C3926B4E061}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {968A6A28-A62D-430A-AEA9-4C3926B4E061}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {968A6A28-A62D-430A-AEA9-4C3926B4E061}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {968A6A28-A62D-430A-AEA9-4C3926B4E061}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {0B613D2B-C8AC-47C1-8B7B-F28B6BB8B95C} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /GameServer.ReverseProxy/ServerEndpointFactory.cs: -------------------------------------------------------------------------------- 1 | namespace GameServer.ReverseProxy; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using Microsoft.Extensions.Logging; 8 | using PlayFab; 9 | using PlayFab.MultiplayerModels; 10 | 11 | public class ServerEndpointFactory 12 | { 13 | private readonly ILogger _logger; 14 | private readonly PlayFabMultiplayerInstanceAPI _multiplayerApi; 15 | 16 | public ServerEndpointFactory(ILoggerFactory loggerFactory, PlayFabMultiplayerInstanceAPI multiplayerApi) 17 | { 18 | _logger = loggerFactory.CreateLogger(); 19 | _multiplayerApi = multiplayerApi; 20 | } 21 | 22 | public async Task GetServerEndpoint(Guid sessionId) 23 | { 24 | var response = await _multiplayerApi.GetMultiplayerServerDetailsAsync(new GetMultiplayerServerDetailsRequest 25 | { 26 | SessionId = sessionId.ToString(), 27 | }); 28 | 29 | if (response.Error?.Error == PlayFabErrorCode.MultiplayerServerNotFound) 30 | { 31 | _logger.LogError("Server not found: Session ID = {SessionId}", sessionId); 32 | 33 | return null; 34 | } 35 | 36 | if (response.Error != null) 37 | { 38 | _logger.LogError("{Request} failed: {Message}", nameof(_multiplayerApi.GetMultiplayerServerDetailsAsync), 39 | response.Error.GenerateErrorReport()); 40 | 41 | throw new Exception(response.Error.GenerateErrorReport()); 42 | } 43 | 44 | var uriBuilder = new UriBuilder(response.Result.FQDN) 45 | { 46 | Port = GetEndpointPortNumber(response.Result.Ports) 47 | }; 48 | 49 | return uriBuilder.ToString(); 50 | } 51 | 52 | private static int GetEndpointPortNumber(IEnumerable ports) 53 | { 54 | // replace this logic with whatever is configured for your build i.e. getting a port by name 55 | return ports.First().Num; 56 | } 57 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repository contains sample code for building a reverse proxy that enables web socket connections over `https` to PlayFab hosted multiplayer servers. This repository is not an official solution, but a starting point for developers who intend to deploy their multiplayer game to a browser. 2 | 3 | ## The problem 4 | 5 | Developers that intend to deploy their multiplayer game on the web face a problem when server connections originate from a `https://` domain. PlayFab hosted game servers are typically hosted from a subdomain of `azure.com`. For security reasons, PlayFab cannot issue SSL certificates to developers to use for `azure.com`. 6 | 7 | Browser security policies require that web socket connections originating from `https://` are created with a secure connection (_web socket over https_ or `wss://`). 8 | 9 | ## A solution 10 | 11 | A reverse proxy can be used to forward requests from a `https://` domain to a PlayFab hosted game server, without the need for an SSL certificate on the game server. 12 | 13 | **Consider the following browser client flow** 14 | 15 | 1. The client retrieves the session ID for an active server. This is typically taken from the result of a call to [Request Multiplayer Server](https://docs.microsoft.com/en-us/rest/api/playfab/multiplayer/multiplayer-server/request-multiplayer-server?view=playfab-rest). 16 | 1. Your game's browser client initiates a connection with the server details 17 | - If you owned the domain `my-domain.com` the request would be `wss://my-domain.com/{sessionId}` 18 | 1. The reverse proxy looks up the server details and forwards the request to the server's fully qualified domain name - a subdomain of `azure.com`. 19 | 20 | ![Connecting to a match diagram](ConnectToReverseProxyDiagram.png "Connecting to a match diagram") 21 | 22 | ### Servers allocated by Matchmaking 23 | 24 | The previous flow could be modified to use the [Matchmaking API](https://docs.microsoft.com/en-us/rest/api/playfab/multiplayer/matchmaking?view=playfab-rest). Session ID would be replaced with the the match ID and queue name returned from [Create Matchmaking Ticket](https://docs.microsoft.com/en-us/rest/api/playfab/multiplayer/matchmaking/create-matchmaking-ticket?view=playfab-rest). 25 | 26 | ### Deployment 27 | 28 | [This Dockerfile](https://github.com/PlayFab/MultiplayerServerSecureWebsocket/blob/main/GameServer.ReverseProxy/Dockerfile) creates a HTTP server that handles requests at `/{sessionId}/{**forwardPath}`. Requests are proxied to a game server in the `Active` state that was requested with [Request Multiplayer Server](https://docs.microsoft.com/en-us/rest/api/playfab/multiplayer/multiplayer-server/request-multiplayer-server?view=playfab-rest). 29 | 30 | https://github.com/PlayFab/MultiplayerServerSecureWebsocket/blob/main/GameServer.ReverseProxy/Startup.cs#L99 31 | 32 | The docker application is intended to be deployed as a web service that is **separate** from your game server. Game servers deployed as HTTP hosts run into the same issue that the repository [MultiplayerServerSecureWebsocket](https://github.com/PlayFab/MultiplayerServerSecureWebsocket) mentions as a problem for games hosted on a https domain - you can't generate a SSL certificate for the fully qualified azure.com domain name of each server session. 33 | 34 | [Azure App Service](https://docs.microsoft.com/en-us/azure/app-service/quickstart-dotnetcore?tabs=net60&pivots=development-environment-vs) is an "off the shelf" solution for the docker application. Linux and Windows should be supported since .NET Core is cross platform. 35 | 36 | ### Integrating with your web client 37 | 38 | When your web client configures a web socket connection i.e. [ConnectEndpoint](https://docs-multiplayer.unity3d.com/docs/0.1.0/api/MLAPI.Transports.UNET.RelayTransport/#connectendpointint32-endpoint-int32-out-byte) with Unity or [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) in client side JavaScript, you'll use the fully qualified domain name from your deployed web service. 39 | 40 | For example, if you created an Azure App Service, the full qualified domain name would be something like `https://myreverseproxy.azurewebsites.net/{sessionId}/` 41 | 42 | ## Need help? 43 | 44 | While this is not an official solution, the best channel to discuss this respository or receive help is to [use the Discussion section](https://github.com/PlayFab/MultiplayerServerSecureWebsocket/discussions). 45 | -------------------------------------------------------------------------------- /GameServer.ReverseProxy/.gitignore: -------------------------------------------------------------------------------- 1 | ### Git Ignore### 2 | 3 | # User-specific files 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # User-specific files (MonoDevelop/Xamarin Studio) 10 | *.userprefs 11 | 12 | # Build results 13 | [Dd]ebug/ 14 | [Dd]ebugPublic/ 15 | [Rr]elease/ 16 | [Rr]eleases/ 17 | x64/ 18 | x86/ 19 | bld/ 20 | [Bb]in/ 21 | [Oo]bj/ 22 | [Ll]og/ 23 | 24 | # Visual Studio 2015-2017 cache/options directory 25 | .vs/ 26 | # Uncomment if you have tasks that create the project's static files in wwwroot 27 | # wwwroot/ 28 | 29 | # MSTest test Results 30 | [Tt]est[Rr]esult*/ 31 | [Bb]uild[Ll]og.* 32 | 33 | # NUNIT 34 | *.VisualState.xml 35 | TestResult.xml 36 | 37 | # Build Results of an ATL Project 38 | [Dd]ebugPS/ 39 | [Rr]eleasePS/ 40 | dlldata.c 41 | 42 | # DNX 43 | project.lock.json 44 | project.fragment.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # Visual Studio code coverage results 113 | *.coverage 114 | *.coveragexml 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | *.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git 😉 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc 265 | 266 | # Cake - Uncomment if you are using it 267 | # tools/ -------------------------------------------------------------------------------- /GameServer.ReverseProxy/Startup.cs: -------------------------------------------------------------------------------- 1 | namespace GameServer.ReverseProxy; 2 | 3 | using System; 4 | using System.IO; 5 | using System.Net; 6 | using System.Net.Http; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Microsoft.AspNetCore.Builder; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.AspNetCore.Routing; 12 | using Microsoft.Extensions.Configuration; 13 | using Microsoft.Extensions.DependencyInjection; 14 | using Microsoft.Extensions.Logging; 15 | using PlayFab; 16 | using PlayFab.AuthenticationModels; 17 | using Yarp.ReverseProxy.Forwarder; 18 | 19 | /// 20 | /// Configured in appsettings.json. Don't check in SecretKey to source control. 21 | /// 22 | public class PlayFabSettings 23 | { 24 | public string TitleId { get; set; } 25 | public string SecretKey { get; set; } 26 | } 27 | 28 | public class Startup 29 | { 30 | private readonly IConfiguration _configuration; 31 | 32 | public Startup(IConfiguration configuration) 33 | { 34 | _configuration = configuration; 35 | } 36 | 37 | public void ConfigureServices(IServiceCollection services) 38 | { 39 | services.AddCors(options => 40 | { 41 | options.AddDefaultPolicy(builder => 42 | { 43 | builder.SetIsOriginAllowed(_ => true) 44 | .AllowAnyMethod() 45 | .AllowAnyHeader() 46 | .AllowCredentials(); 47 | }); 48 | }); 49 | 50 | services.AddHttpForwarder(); 51 | services.AddSingleton(_ => 52 | { 53 | var playfabConfig = _configuration.GetSection("PlayFab").Get(); 54 | 55 | return new(new PlayFabApiSettings 56 | { 57 | TitleId = playfabConfig.TitleId, 58 | DeveloperSecretKey = playfabConfig.SecretKey, 59 | }); 60 | }); 61 | services.AddTransient(context => 62 | { 63 | var authApi = context.GetRequiredService(); 64 | 65 | // TODO: this should be cached until expiration (1 day) 66 | var entityToken = authApi.GetEntityTokenAsync(new GetEntityTokenRequest()); 67 | 68 | return new(authApi.apiSettings, 69 | new PlayFabAuthenticationContext() 70 | { 71 | EntityToken = entityToken.Result.Result.EntityToken 72 | }); 73 | }); 74 | services.AddSingleton(); 75 | services.AddReverseProxy().LoadFromConfig(_configuration.GetSection("ReverseProxy")); 76 | } 77 | 78 | public void Configure(IApplicationBuilder app, IConfiguration configuration, IHttpForwarder forwarder) 79 | { 80 | var httpClient = new HttpMessageInvoker(new SocketsHttpHandler 81 | { 82 | UseProxy = false, 83 | AllowAutoRedirect = false, 84 | AutomaticDecompression = DecompressionMethods.None, 85 | UseCookies = true 86 | }); 87 | 88 | var requestOptions = new ForwarderRequestConfig 89 | { 90 | ActivityTimeout = TimeSpan.FromSeconds(5) 91 | }; 92 | var transformer = new GameServerRequestTransformer(); 93 | 94 | app.UseRouting(); 95 | app.UseEndpoints(endpoints => 96 | { 97 | var logger = endpoints.ServiceProvider.GetRequiredService().CreateLogger("ProxyEndpointHandler"); 98 | 99 | endpoints.Map("/{sessionId:guid}/{**forwardPath}", async context => 100 | { 101 | var detailsFactory = context.RequestServices.GetRequiredService(); 102 | 103 | var routeValues = context.GetRouteData().Values; 104 | 105 | // respond with 400 Bad Request when the request path doesn't have the expected format 106 | if (!Guid.TryParse(routeValues["sessionId"]?.ToString(), out var sessionId)) 107 | { 108 | context.Response.StatusCode = (int) HttpStatusCode.BadRequest; 109 | 110 | return; 111 | } 112 | 113 | string serverEndpoint = null; 114 | 115 | try 116 | { 117 | serverEndpoint = await detailsFactory.GetServerEndpoint(sessionId); 118 | } 119 | catch (Exception) 120 | { 121 | context.Response.StatusCode = (int) HttpStatusCode.InternalServerError; 122 | } 123 | 124 | // We couldn't find a server with this build/session/region 125 | // The client should use the 404 status code to display a useful message like "This server was not found or is no longer available" 126 | if (serverEndpoint == null) 127 | { 128 | context.Response.StatusCode = (int) HttpStatusCode.NotFound; 129 | 130 | return; 131 | } 132 | 133 | await forwarder.SendAsync(context, 134 | serverEndpoint, 135 | httpClient, requestOptions, 136 | transformer); 137 | }); 138 | }); 139 | 140 | app.UseCors(); 141 | } 142 | } 143 | 144 | /// 145 | /// Forwards the request path and query parameters to given game server URL 146 | /// 147 | /// /{sessionId}/some/path?test=true is mapped to {serverUrl}/some/path?test=true 148 | internal class GameServerRequestTransformer : HttpTransformer 149 | { 150 | public override async ValueTask TransformRequestAsync(HttpContext httpContext, 151 | HttpRequestMessage proxyRequest, string serverEndpoint, CancellationToken cancellationToken) 152 | { 153 | await base.TransformRequestAsync(httpContext, proxyRequest, serverEndpoint, cancellationToken); 154 | 155 | var builder = new UriBuilder(serverEndpoint) 156 | { 157 | Query = httpContext.Request.QueryString.ToString() 158 | }; 159 | 160 | var forwardPath = httpContext.GetRouteValue("forwardPath"); 161 | 162 | if (forwardPath != null) 163 | { 164 | builder.Path = Path.Combine(builder.Path, forwardPath.ToString() ?? string.Empty); 165 | } 166 | 167 | proxyRequest.RequestUri = builder.Uri; 168 | } 169 | } --------------------------------------------------------------------------------