2017-09-13 08:47:34 +02:00
using Microsoft.AspNetCore.Http ;
using Microsoft.Extensions.Logging ;
using Microsoft.Extensions.Primitives ;
using System ;
using System.Collections.Generic ;
using System.Text ;
using System.Linq ;
using System.Threading.Tasks ;
using System.IO ;
using BTCPayServer.Authentication ;
using BTCPayServer.Logging ;
using Newtonsoft.Json ;
using BTCPayServer.Models ;
using BTCPayServer.Configuration ;
2018-02-15 08:17:27 +01:00
using System.Net.WebSockets ;
2018-04-29 13:32:43 +02:00
using BTCPayServer.Services.Stores ;
2017-09-13 08:47:34 +02:00
namespace BTCPayServer.Hosting
{
2017-10-27 10:53:04 +02:00
public class BTCPayMiddleware
{
RequestDelegate _Next ;
2017-12-02 15:22:23 +01:00
BTCPayServerOptions _Options ;
2017-12-16 17:04:20 +01:00
2017-10-27 10:53:04 +02:00
public BTCPayMiddleware ( RequestDelegate next ,
2017-12-17 11:58:55 +01:00
BTCPayServerOptions options )
2017-10-27 10:53:04 +02:00
{
_Next = next ? ? throw new ArgumentNullException ( nameof ( next ) ) ;
2017-12-02 15:22:23 +01:00
_Options = options ? ? throw new ArgumentNullException ( nameof ( options ) ) ;
2017-10-27 10:53:04 +02:00
}
2017-09-13 08:47:34 +02:00
2017-10-12 09:33:53 +02:00
2017-10-27 10:53:04 +02:00
public async Task Invoke ( HttpContext httpContext )
{
2017-12-16 17:04:20 +01:00
RewriteHostIfNeeded ( httpContext ) ;
2018-04-29 13:32:43 +02:00
2018-04-27 19:51:20 +02:00
try
2017-10-27 10:53:04 +02:00
{
2018-04-29 13:32:43 +02:00
var bitpayAuth = GetBitpayAuth ( httpContext , out bool isBitpayAuth ) ;
var isBitpayAPI = IsBitpayAPI ( httpContext , isBitpayAuth ) ;
httpContext . SetIsBitpayAPI ( isBitpayAPI ) ;
if ( isBitpayAPI )
2018-04-29 11:28:04 +02:00
{
2018-04-30 15:28:00 +02:00
httpContext . SetBitpayAuth ( bitpayAuth ) ;
2018-04-29 13:32:43 +02:00
}
2017-10-27 10:53:04 +02:00
await _Next ( httpContext ) ;
}
2018-02-15 08:17:27 +01:00
catch ( WebSocketException )
{ }
2017-10-27 10:53:04 +02:00
catch ( UnauthorizedAccessException ex )
{
await HandleBitpayHttpException ( httpContext , new BitpayHttpException ( 401 , ex . Message ) ) ;
}
catch ( BitpayHttpException ex )
{
await HandleBitpayHttpException ( httpContext , ex ) ;
}
catch ( Exception ex )
{
Logs . PayServer . LogCritical ( new EventId ( ) , ex , "Unhandled exception in BTCPayMiddleware" ) ;
throw ;
}
2018-04-29 11:28:04 +02:00
}
2017-09-13 08:47:34 +02:00
2018-04-29 13:32:43 +02:00
private static ( string Signature , String Id , String Authorization ) GetBitpayAuth ( HttpContext httpContext , out bool hasBitpayAuth )
{
httpContext . Request . Headers . TryGetValue ( "x-signature" , out StringValues values ) ;
var sig = values . FirstOrDefault ( ) ;
httpContext . Request . Headers . TryGetValue ( "x-identity" , out values ) ;
var id = values . FirstOrDefault ( ) ;
httpContext . Request . Headers . TryGetValue ( "Authorization" , out values ) ;
var auth = values . FirstOrDefault ( ) ;
hasBitpayAuth = auth ! = null | | ( sig ! = null & & id ! = null ) ;
return ( sig , id , auth ) ;
}
private bool IsBitpayAPI ( HttpContext httpContext , bool bitpayAuth )
{
if ( ! httpContext . Request . Path . HasValue )
return false ;
2018-05-11 15:38:31 +02:00
var isJson = ( httpContext . Request . ContentType ? ? string . Empty ) . StartsWith ( "application/json" , StringComparison . OrdinalIgnoreCase ) ;
2018-04-29 13:32:43 +02:00
var path = httpContext . Request . Path . Value ;
if (
bitpayAuth & &
path = = "/invoices" & &
httpContext . Request . Method = = "POST" & &
2018-05-11 15:38:31 +02:00
isJson )
2018-04-29 13:32:43 +02:00
return true ;
if (
bitpayAuth & &
path = = "/invoices" & &
httpContext . Request . Method = = "GET" )
return true ;
if (
path . StartsWith ( "/invoices/" , StringComparison . OrdinalIgnoreCase ) & &
2018-05-11 10:05:08 +02:00
httpContext . Request . Method = = "GET" & &
2018-05-11 15:38:31 +02:00
( isJson | | httpContext . Request . Query . ContainsKey ( "token" ) ) )
2018-04-29 13:32:43 +02:00
return true ;
2018-07-27 07:55:42 +02:00
if ( path . StartsWith ( "/rates" , StringComparison . OrdinalIgnoreCase ) & &
2018-04-29 13:32:43 +02:00
httpContext . Request . Method = = "GET" )
return true ;
if (
2018-10-06 16:20:01 +02:00
path . Equals ( "/tokens" , StringComparison . Ordinal ) & &
2018-04-29 13:32:43 +02:00
( httpContext . Request . Method = = "GET" | | httpContext . Request . Method = = "POST" ) )
return true ;
return false ;
}
2017-12-16 17:04:20 +01:00
private void RewriteHostIfNeeded ( HttpContext httpContext )
{
2018-01-09 08:54:40 +01:00
string reverseProxyScheme = null ;
if ( httpContext . Request . Headers . TryGetValue ( "X-Forwarded-Proto" , out StringValues proto ) )
{
var scheme = proto . SingleOrDefault ( ) ;
if ( scheme ! = null )
{
reverseProxyScheme = scheme ;
}
}
ushort? reverseProxyPort = null ;
if ( httpContext . Request . Headers . TryGetValue ( "X-Forwarded-Port" , out StringValues port ) )
{
var portString = port . SingleOrDefault ( ) ;
if ( portString ! = null & & ushort . TryParse ( portString , out ushort pp ) )
{
reverseProxyPort = pp ;
}
}
2017-12-16 17:04:20 +01:00
// Make sure that code executing after this point think that the external url has been hit.
if ( _Options . ExternalUrl ! = null )
{
2018-01-09 08:54:40 +01:00
if ( reverseProxyScheme ! = null & & _Options . ExternalUrl . Scheme ! = reverseProxyScheme )
{
if ( reverseProxyScheme = = "http" & & _Options . ExternalUrl . Scheme = = "https" )
2018-10-06 16:20:01 +02:00
Logs . PayServer . LogWarning ( $"BTCPay ExternalUrl setting expected to use scheme '{_Options.ExternalUrl.Scheme}' externally, but the reverse proxy uses scheme '{reverseProxyScheme}' (X-Forwarded-Port), forcing ExternalUrl" ) ;
2018-01-09 08:54:40 +01:00
}
2018-10-06 16:20:01 +02:00
httpContext . Request . Scheme = _Options . ExternalUrl . Scheme ;
2017-12-16 17:04:20 +01:00
if ( _Options . ExternalUrl . IsDefaultPort )
httpContext . Request . Host = new HostString ( _Options . ExternalUrl . Host ) ;
else
2018-01-09 08:54:40 +01:00
{
if ( reverseProxyPort ! = null & & _Options . ExternalUrl . Port ! = reverseProxyPort . Value )
{
Logs . PayServer . LogWarning ( $"BTCPay ExternalUrl setting expected to use port '{_Options.ExternalUrl.Port}' externally, but the reverse proxy uses port '{reverseProxyPort.Value}'" ) ;
httpContext . Request . Host = new HostString ( _Options . ExternalUrl . Host , reverseProxyPort . Value ) ;
}
else
{
httpContext . Request . Host = new HostString ( _Options . ExternalUrl . Host , _Options . ExternalUrl . Port ) ;
}
}
2017-12-16 17:04:20 +01:00
}
// NGINX pass X-Forwarded-Proto and X-Forwarded-Port, so let's use that to have better guess of the real domain
else
{
ushort? p = null ;
2018-01-09 08:54:40 +01:00
if ( reverseProxyScheme ! = null )
2017-12-16 17:04:20 +01:00
{
2018-01-09 08:54:40 +01:00
httpContext . Request . Scheme = reverseProxyScheme ;
if ( reverseProxyScheme = = "http" )
p = 80 ;
if ( reverseProxyScheme = = "https" )
p = 443 ;
2017-12-16 17:04:20 +01:00
}
2018-01-09 08:54:40 +01:00
if ( reverseProxyPort ! = null )
2017-12-16 17:04:20 +01:00
{
2018-01-09 08:54:40 +01:00
p = reverseProxyPort . Value ;
2017-12-16 17:04:20 +01:00
}
2018-01-09 08:54:40 +01:00
2017-12-16 17:04:20 +01:00
if ( p . HasValue )
{
bool isDefault = httpContext . Request . Scheme = = "http" & & p . Value = = 80 ;
isDefault | = httpContext . Request . Scheme = = "https" & & p . Value = = 443 ;
if ( isDefault )
httpContext . Request . Host = new HostString ( httpContext . Request . Host . Host ) ;
else
httpContext . Request . Host = new HostString ( httpContext . Request . Host . Host , p . Value ) ;
}
}
}
2017-10-27 10:53:04 +02:00
private static async Task HandleBitpayHttpException ( HttpContext httpContext , BitpayHttpException ex )
{
httpContext . Response . StatusCode = ex . StatusCode ;
using ( var writer = new StreamWriter ( httpContext . Response . Body , new UTF8Encoding ( false ) , 1024 , true ) )
{
httpContext . Response . ContentType = "application/json" ;
var result = JsonConvert . SerializeObject ( new BitpayErrorsModel ( ex ) ) ;
writer . Write ( result ) ;
await writer . FlushAsync ( ) ;
}
}
}
2017-09-13 08:47:34 +02:00
}