mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-02 19:38:24 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
155 lines
4.9 KiB
C#
155 lines
4.9 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Lets your game make async HTTP requests.
|
|
/// </summary>
|
|
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 );
|
|
}
|
|
|
|
/// <summary>
|
|
/// We shouldn't blindly let users opt into local http.
|
|
/// But it's okay for editor, dedicated servers and standalone.
|
|
/// </summary>
|
|
internal static bool IsLocalAllowed => ((Application.IsEditor || Application.IsDedicatedServer) && CommandLine.HasSwitch( "-allowlocalhttp" )) || Application.IsStandalone;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
/// <param name="uri">The Uri to check.</param>
|
|
/// <returns>True if the Uri can be accessed, false if the Uri will be blocked.</returns>
|
|
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<string> 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
|
|
};
|
|
|
|
/// <summary>
|
|
/// Checks if a given header is allowed to be set.
|
|
/// </summary>
|
|
/// <param name="header">The header name to check.</param>
|
|
/// <returns>True if the header is allowed to be set.</returns>
|
|
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<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken )
|
|
{
|
|
HandleRequest( request );
|
|
return base.SendAsync( request, cancellationToken );
|
|
}
|
|
}
|