btcpayserver/BTCPayServer/Hosting/BTCpayMiddleware.cs
2024-03-30 10:20:24 +01:00

164 lines
6.6 KiB
C#

using System;
using System.Globalization;
using System.Linq;
using System.Net.WebSockets;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Configuration;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
namespace BTCPayServer.Hosting
{
public class BTCPayMiddleware
{
readonly RequestDelegate _Next;
readonly BTCPayServerOptions _Options;
public Logs Logs { get; }
readonly BTCPayServerEnvironment _Env;
public BTCPayMiddleware(RequestDelegate next,
BTCPayServerOptions options,
BTCPayServerEnvironment env,
Logs logs)
{
_Env = env ?? throw new ArgumentNullException(nameof(env));
_Next = next ?? throw new ArgumentNullException(nameof(next));
_Options = options ?? throw new ArgumentNullException(nameof(options));
Logs = logs;
}
public async Task Invoke(HttpContext httpContext)
{
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
try
{
var bitpayAuth = GetBitpayAuth(httpContext, out bool isBitpayAuth);
var isBitpayAPI = IsBitpayAPI(httpContext, isBitpayAuth);
if (isBitpayAPI && httpContext.Request.Method == "OPTIONS")
{
httpContext.Response.StatusCode = 200;
httpContext.Response.SetHeader("Access-Control-Allow-Origin", "*");
if (httpContext.Request.Headers.ContainsKey("Access-Control-Request-Headers"))
{
httpContext.Response.SetHeader("Access-Control-Allow-Headers", httpContext.Request.Headers["Access-Control-Request-Headers"].FirstOrDefault());
}
return; // We bypass MVC completely
}
httpContext.SetIsBitpayAPI(isBitpayAPI);
if (isBitpayAPI)
{
httpContext.Response.SetHeader("Access-Control-Allow-Origin", "*");
httpContext.SetBitpayAuth(bitpayAuth);
await _Next(httpContext);
return;
}
var isHtml = httpContext.Request.Headers.TryGetValue("Accept", out var accept)
&& accept.ToString().StartsWith("text/html", StringComparison.OrdinalIgnoreCase);
var isModal = httpContext.Request.Query.TryGetValue("view", out var view)
&& view.ToString().Equals("modal", StringComparison.OrdinalIgnoreCase);
if (!string.IsNullOrEmpty(_Env.OnionUrl) &&
!httpContext.Request.IsOnion() &&
isHtml &&
!isModal)
{
var onionLocation = _Env.OnionUrl + httpContext.Request.GetEncodedPathAndQuery();
httpContext.Response.SetHeader("Onion-Location", onionLocation);
}
}
catch (WebSocketException)
{ }
catch (UnauthorizedAccessException ex)
{
await HandleBitpayHttpException(httpContext, new BitpayHttpException(401, ex.Message));
return;
}
catch (BitpayHttpException ex)
{
await HandleBitpayHttpException(httpContext, ex);
return;
}
catch (Exception ex)
{
Logs.PayServer.LogCritical(new EventId(), ex, "Unhandled exception in BTCPayMiddleware");
throw;
}
await _Next(httpContext);
}
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;
// In case of anyone can create invoice, the storeId can be set explicitly
bitpayAuth |= httpContext.Request.Query.ContainsKey("storeid");
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
var path = httpContext.Request.Path.Value;
var method = httpContext.Request.Method;
var isCors = method == "OPTIONS";
if (
(isCors || bitpayAuth) &&
(path == "/invoices" || path == "/invoices/") &&
(isCors || (method == "POST" && isJson)))
return true;
if (
(isCors || bitpayAuth) &&
(path == "/invoices" || path == "/invoices/") &&
(isCors || method == "GET"))
return true;
if (
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
(isCors || method == "GET") &&
(isCors || isJson || httpContext.Request.Query.ContainsKey("token")))
return true;
if (path.StartsWith("/rates", StringComparison.OrdinalIgnoreCase) &&
(isCors || method == "GET"))
return true;
if (
path.Equals("/tokens", StringComparison.Ordinal) &&
(isCors || method == "GET" || method == "POST"))
return true;
return false;
}
private static async Task HandleBitpayHttpException(HttpContext httpContext, BitpayHttpException ex)
{
httpContext.Response.StatusCode = ex.StatusCode;
httpContext.Response.ContentType = "application/json";
var result = JsonConvert.SerializeObject(new BitpayErrorsModel(ex));
await httpContext.Response.WriteAsync(result);
}
}
}