using Sandbox.Utility; using System; using System.Net; using System.Net.Http; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; namespace Sandbox; /// /// Lets your game make async HTTP requests. /// public static partial class Http { internal const string UserAgent = "facepunch-sbox"; // todo: add version? internal const string Referrer = "https://sbox.facepunch.com/"; // todo: link to current gamemode? private static readonly HttpClient Client; static Http() { var socketHttpHandler = new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes( 2 ), }; // Gives us 1 http client per game, so cookies don't persist etc. Client = new HttpClient( new SboxHttpHandler( socketHttpHandler ) ); Client.Timeout = TimeSpan.FromMinutes( 120 ); } /// /// We shouldn't blindly let users opt into local http. /// But it's okay for editor, dedicated servers and standalone. /// internal static bool IsLocalAllowed => ((Application.IsEditor || Application.IsDedicatedServer) && CommandLine.HasSwitch( "-allowlocalhttp" )) || Application.IsStandalone; /// /// Check if the given Uri matches the following requirements: /// 1. Scheme is https/http or wss/ws /// 2. If it's localhost, only allow ports 80/443/8080/8443 /// 3. Not an ip address /// /// The Uri to check. /// True if the Uri can be accessed, false if the Uri will be blocked. public static bool IsAllowed( Uri uri ) { if ( uri.Scheme != "http" && uri.Scheme != "https" && uri.Scheme != "wss" && uri.Scheme != "ws" ) return false; if ( IsLocalAllowed ) return true; // Allow specific ports for loopback (localhost) URIs so that people can do local development/testing // Only including the obvious development server only ports because nothing should conflict with these if ( uri.IsLoopback ) return uri.IsDefaultPort || uri.Port is 80 or 443 or 8080 or 8443; // don't allow ip urls (unless it's covered by loopback above) if ( uri.HostNameType == UriHostNameType.IPv4 || uri.HostNameType == UriHostNameType.IPv6 ) return false; try { // don't allow any domains that resolve to private or loopback ip addresses // shit routers and internet of shit devices are typically vulnerable // https://medium.com/@brannondorsey/attacking-private-networks-from-the-internet-with-dns-rebinding-ea7098a2d325 if ( uri.IsPrivate() ) return false; } catch ( System.Net.Sockets.SocketException ) { // No such host is known return false; } return true; } // https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name private static readonly HashSet ForbiddenHeaders = new( StringComparer.InvariantCultureIgnoreCase ) { "Accept-Charset", "Accept-Encoding", "Access-Control-Request-Headers", "Access-Control-Request-Method", "Connection", "Content-Length", //"Cookie", // cookies are necessary for us //"Cookie2", "Date", "DNT", "Expect", "Feature-Policy", "Host", "Keep-Alive", "Origin", // we should set this (preferably with a way to identify the gamemode) "Referer", "TE", "Trailer", "Transfer-Encoding", "Upgrade", "Via", "User-Agent", // not forbidden officially but we'll be setting this to something s&box specific }; /// /// Checks if a given header is allowed to be set. /// /// The header name to check. /// True if the header is allowed to be set. public static bool IsHeaderAllowed( string header ) { return !string.IsNullOrWhiteSpace( header ) && !ForbiddenHeaders.Contains( header ) && !header.StartsWith( "Proxy-", StringComparison.InvariantCultureIgnoreCase ) && !header.StartsWith( "Sec-", StringComparison.InvariantCultureIgnoreCase ); } } internal sealed class SboxHttpHandler : DelegatingHandler { public SboxHttpHandler( HttpMessageHandler innerHandler ) : base( innerHandler ) { } private static void HandleRequest( HttpRequestMessage request ) { // Check URI here because of redirects if ( !Http.IsAllowed( request.RequestUri ) ) { throw new InvalidOperationException( $"Access to '{request.RequestUri}' is not allowed." ); } request.Headers.Remove( "User-Agent" ); request.Headers.TryAddWithoutValidation( "User-Agent", Http.UserAgent ); request.Headers.Remove( "Referer" ); request.Headers.TryAddWithoutValidation( "Referer", Http.Referrer ); } protected override HttpResponseMessage Send( HttpRequestMessage request, CancellationToken cancellationToken ) { HandleRequest( request ); return base.Send( request, cancellationToken ); } protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken ) { HandleRequest( request ); return base.SendAsync( request, cancellationToken ); } }