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 NBitcoin ;
using NBitcoin.Crypto ;
using NBitcoin.DataEncoders ;
using Microsoft.AspNetCore.Http.Internal ;
using System.IO ;
using BTCPayServer.Authentication ;
using System.Security.Principal ;
using NBitpayClient.Extensions ;
using BTCPayServer.Logging ;
using Newtonsoft.Json ;
using BTCPayServer.Models ;
using BTCPayServer.Configuration ;
2017-09-27 08:16:30 +02:00
using Microsoft.AspNetCore.Mvc ;
using Microsoft.AspNetCore.Mvc.Routing ;
using Microsoft.AspNetCore.Http.Extensions ;
2017-10-12 09:33:53 +02:00
using BTCPayServer.Controllers ;
2018-02-15 08:17:27 +01:00
using System.Net.WebSockets ;
2018-04-27 19:09:24 +02:00
using System.Security.Claims ;
using BTCPayServer.Services ;
2018-04-27 19:51:20 +02:00
using NBitpayClient ;
using Newtonsoft.Json.Linq ;
2017-09-13 08:47:34 +02:00
namespace BTCPayServer.Hosting
{
2017-10-27 10:53:04 +02:00
public class BTCPayMiddleware
{
TokenRepository _TokenRepository ;
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 ,
TokenRepository tokenRepo ,
2017-12-17 11:58:55 +01:00
BTCPayServerOptions options )
2017-10-27 10:53:04 +02:00
{
_TokenRepository = tokenRepo ? ? throw new ArgumentNullException ( nameof ( tokenRepo ) ) ;
_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 ) ;
2017-10-27 10:53:04 +02:00
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 ( ) ;
2018-04-29 11:28:04 +02:00
httpContext . Request . Headers . TryGetValue ( "Authorization" , out values ) ;
var auth = values . FirstOrDefault ( ) ;
2018-04-27 19:51:20 +02:00
try
2017-10-27 10:53:04 +02:00
{
2018-04-29 11:28:04 +02:00
bool isBitId = false ;
2018-04-27 19:51:20 +02:00
if ( ! string . IsNullOrEmpty ( sig ) & & ! string . IsNullOrEmpty ( id ) )
2017-10-27 10:53:04 +02:00
{
2018-04-27 19:51:20 +02:00
await HandleBitId ( httpContext , sig , id ) ;
2018-04-29 11:28:04 +02:00
isBitId = httpContext . User . HasClaim ( c = > c . Type = = Claims . SIN ) ;
if ( ! isBitId )
Logs . PayServer . LogDebug ( "BitId signature check failed" ) ;
2017-10-27 10:53:04 +02:00
}
2018-04-29 11:28:04 +02:00
if ( ! isBitId & & ! string . IsNullOrEmpty ( auth ) )
{
await HandleLegacyAPIKey ( httpContext , auth ) ;
}
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
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" )
Logs . PayServer . LogWarning ( $"BTCPay ExternalUrl setting expected to use scheme '{_Options.ExternalUrl.Scheme}' externally, but the reverse proxy uses scheme '{reverseProxyScheme}'" ) ;
httpContext . Request . Scheme = reverseProxyScheme ;
}
else
2018-04-27 19:51:20 +02:00
{
2018-01-09 08:54:40 +01: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 ( ) ;
}
}
2018-04-27 19:51:20 +02:00
private async Task HandleBitId ( HttpContext httpContext , string sig , string id )
{
httpContext . Request . EnableRewind ( ) ;
string body = string . Empty ;
if ( httpContext . Request . ContentLength ! = 0 & & httpContext . Request . Body ! = null )
{
using ( StreamReader reader = new StreamReader ( httpContext . Request . Body , Encoding . UTF8 , true , 1024 , true ) )
{
body = reader . ReadToEnd ( ) ;
}
httpContext . Request . Body . Position = 0 ;
}
var url = httpContext . Request . GetEncodedUrl ( ) ;
try
{
var key = new PubKey ( id ) ;
if ( BitIdExtensions . CheckBitIDSignature ( key , sig , url , body ) )
{
var sin = key . GetBitIDSIN ( ) ;
var identity = ( ( ClaimsIdentity ) httpContext . User . Identity ) ;
identity . AddClaim ( new Claim ( Claims . SIN , sin ) ) ;
string token = null ;
if ( httpContext . Request . Query . TryGetValue ( "token" , out var tokenValues ) )
{
token = tokenValues [ 0 ] ;
}
if ( token = = null & & ! String . IsNullOrEmpty ( body ) & & httpContext . Request . Method = = "POST" )
{
try
{
token = JObject . Parse ( body ) ? . Property ( "token" ) ? . Value ? . Value < string > ( ) ;
}
catch { }
}
if ( token ! = null )
{
var bitToken = await GetTokenPermissionAsync ( sin , token ) ;
if ( bitToken = = null )
{
throw new BitpayHttpException ( 401 , $"This endpoint does not support this facade" ) ;
}
identity . AddClaim ( new Claim ( Claims . OwnStore , bitToken . StoreId ) ) ;
}
Logs . PayServer . LogDebug ( $"BitId signature check success for SIN {sin}" ) ;
2018-04-29 11:28:04 +02:00
NBitcoin . Extensions . TryAdd ( httpContext . Items , "IsBitpayAPI" , true ) ;
2018-04-27 19:51:20 +02:00
}
}
catch ( FormatException ) { }
2018-04-29 11:28:04 +02:00
}
private async Task HandleLegacyAPIKey ( HttpContext httpContext , string auth )
{
var splitted = auth . Split ( ' ' , StringSplitOptions . RemoveEmptyEntries ) ;
if ( splitted . Length ! = 2 | | ! splitted [ 0 ] . Equals ( "Basic" , StringComparison . OrdinalIgnoreCase ) )
{
throw new BitpayHttpException ( 401 , $"Invalid Authorization header" ) ;
}
string apiKey = null ;
try
{
apiKey = Encoders . ASCII . EncodeData ( Encoders . Base64 . DecodeData ( splitted [ 1 ] ) ) ;
}
catch
{
throw new BitpayHttpException ( 401 , $"Invalid Authorization header" ) ;
}
var storeId = await _TokenRepository . GetStoreIdFromAPIKey ( apiKey ) ;
if ( storeId = = null )
{
throw new BitpayHttpException ( 401 , $"Invalid Authorization header" ) ;
}
var identity = ( ( ClaimsIdentity ) httpContext . User . Identity ) ;
identity . AddClaim ( new Claim ( Claims . OwnStore , storeId ) ) ;
NBitcoin . Extensions . TryAdd ( httpContext . Items , "IsBitpayAPI" , true ) ;
2018-04-27 19:51:20 +02:00
}
private async Task < BitTokenEntity > GetTokenPermissionAsync ( string sin , string expectedToken )
{
var actualTokens = ( await _TokenRepository . GetTokens ( sin ) ) . ToArray ( ) ;
actualTokens = actualTokens . SelectMany ( t = > GetCompatibleTokens ( t ) ) . ToArray ( ) ;
var actualToken = actualTokens . FirstOrDefault ( a = > a . Value . Equals ( expectedToken , StringComparison . Ordinal ) ) ;
if ( expectedToken = = null | | actualToken = = null )
{
Logs . PayServer . LogDebug ( $"No token found for facade {Facade.Merchant} for SIN {sin}" ) ;
return null ;
}
return actualToken ;
}
private IEnumerable < BitTokenEntity > GetCompatibleTokens ( BitTokenEntity token )
{
if ( token . Facade = = Facade . Merchant . ToString ( ) )
{
yield return token . Clone ( Facade . User ) ;
yield return token . Clone ( Facade . PointOfSale ) ;
}
if ( token . Facade = = Facade . PointOfSale . ToString ( ) )
{
yield return token . Clone ( Facade . User ) ;
}
yield return token ;
}
2017-10-27 10:53:04 +02:00
}
2017-09-13 08:47:34 +02:00
}