mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-23 14:40:36 +01:00
427 lines
18 KiB
C#
427 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.WebUtilities;
|
|
using Microsoft.Extensions.Primitives;
|
|
using NBitcoin.DataEncoders;
|
|
|
|
namespace BTCPayServer.Payments.Lightning
|
|
{
|
|
public enum LightningConnectionType
|
|
{
|
|
Charge,
|
|
CLightning,
|
|
LndREST
|
|
}
|
|
public class LightningConnectionString
|
|
{
|
|
static Dictionary<string, LightningConnectionType> typeMapping;
|
|
static Dictionary<LightningConnectionType, string> typeMappingReverse;
|
|
static LightningConnectionString()
|
|
{
|
|
typeMapping = new Dictionary<string, LightningConnectionType>();
|
|
typeMapping.Add("clightning", LightningConnectionType.CLightning);
|
|
typeMapping.Add("charge", LightningConnectionType.Charge);
|
|
typeMapping.Add("lnd-rest", LightningConnectionType.LndREST);
|
|
typeMappingReverse = new Dictionary<LightningConnectionType, string>();
|
|
foreach (var kv in typeMapping)
|
|
{
|
|
typeMappingReverse.Add(kv.Value, kv.Key);
|
|
}
|
|
}
|
|
public static bool TryParse(string str, bool supportLegacy, out LightningConnectionString connectionString)
|
|
{
|
|
return TryParse(str, supportLegacy, out connectionString, out var error);
|
|
}
|
|
public static bool TryParse(string str, bool supportLegacy, out LightningConnectionString connectionString, out string error)
|
|
{
|
|
if (str == null)
|
|
throw new ArgumentNullException(nameof(str));
|
|
|
|
if (supportLegacy)
|
|
{
|
|
var parsed = TryParseLegacy(str, out connectionString, out error);
|
|
if (!parsed)
|
|
{
|
|
parsed = TryParseNewFormat(str, out connectionString, out error);
|
|
}
|
|
return parsed;
|
|
}
|
|
else
|
|
{
|
|
return TryParseNewFormat(str, out connectionString, out error);
|
|
}
|
|
}
|
|
|
|
private static bool TryParseNewFormat(string str, out LightningConnectionString connectionString, out string error)
|
|
{
|
|
connectionString = null;
|
|
error = null;
|
|
var parts = str.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
|
|
Dictionary<string, string> keyValues = new Dictionary<string, string>();
|
|
foreach (var part in parts.Select(p => p.Trim()))
|
|
{
|
|
var idx = part.IndexOf('=', StringComparison.OrdinalIgnoreCase);
|
|
if (idx == -1)
|
|
{
|
|
error = "The format of the connectionString should a list of key=value delimited by semicolon";
|
|
return false;
|
|
}
|
|
var key = part.Substring(0, idx).Trim().ToLowerInvariant();
|
|
var value = part.Substring(idx + 1).Trim();
|
|
if (keyValues.ContainsKey(key))
|
|
{
|
|
error = $"Duplicate key {key}";
|
|
return false;
|
|
}
|
|
keyValues.Add(key, value);
|
|
}
|
|
|
|
var possibleTypes = String.Join(", ", typeMapping.Select(k => k.Key).ToArray());
|
|
|
|
LightningConnectionString result = new LightningConnectionString();
|
|
var type = Take(keyValues, "type");
|
|
if (type == null)
|
|
{
|
|
error = $"The key 'type' is mandatory, possible values are {possibleTypes}";
|
|
return false;
|
|
}
|
|
|
|
if (!typeMapping.TryGetValue(type.ToLowerInvariant(), out var connectionType))
|
|
{
|
|
error = $"The key 'type' is invalid, possible values are {possibleTypes}";
|
|
return false;
|
|
}
|
|
|
|
result.ConnectionType = connectionType;
|
|
|
|
switch (connectionType)
|
|
{
|
|
case LightningConnectionType.Charge:
|
|
{
|
|
var server = Take(keyValues, "server");
|
|
if (server == null)
|
|
{
|
|
error = $"The key 'server' is mandatory for charge connection strings";
|
|
return false;
|
|
}
|
|
|
|
if (!Uri.TryCreate(server, UriKind.Absolute, out var uri)
|
|
|| (uri.Scheme != "http" && uri.Scheme != "https"))
|
|
{
|
|
error = $"The key 'server' should be an URI starting by http:// or https://";
|
|
return false;
|
|
}
|
|
|
|
parts = uri.UserInfo.Split(':');
|
|
if (!string.IsNullOrEmpty(uri.UserInfo) && parts.Length == 2)
|
|
{
|
|
result.Username = parts[0];
|
|
result.Password = parts[1];
|
|
}
|
|
else
|
|
{
|
|
var apiToken = Take(keyValues, "api-token");
|
|
if (apiToken == null)
|
|
{
|
|
error = "The key 'api-token' is not found";
|
|
return false;
|
|
}
|
|
result.Username = "api-token";
|
|
result.Password = apiToken;
|
|
}
|
|
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
|
|
}
|
|
break;
|
|
case LightningConnectionType.CLightning:
|
|
{
|
|
var server = Take(keyValues, "server");
|
|
if (server == null)
|
|
{
|
|
error = $"The key 'server' is mandatory for charge connection strings";
|
|
return false;
|
|
}
|
|
|
|
if (server.StartsWith("//", StringComparison.OrdinalIgnoreCase))
|
|
server = "unix:" + str;
|
|
else if (server.StartsWith("/", StringComparison.OrdinalIgnoreCase))
|
|
server = "unix:/" + str;
|
|
|
|
if (!Uri.TryCreate(server, UriKind.Absolute, out var uri)
|
|
|| (uri.Scheme != "tcp" && uri.Scheme != "unix"))
|
|
{
|
|
error = $"The key 'server' should be an URI starting by tcp:// or unix:// or a path to the 'lightning-rpc' unix socket";
|
|
return false;
|
|
}
|
|
result.BaseUri = uri;
|
|
}
|
|
break;
|
|
case LightningConnectionType.LndREST:
|
|
{
|
|
var server = Take(keyValues, "server");
|
|
if (server == null)
|
|
{
|
|
error = $"The key 'server' is mandatory for lnd connection strings";
|
|
return false;
|
|
}
|
|
if (!Uri.TryCreate(server, UriKind.Absolute, out var uri)
|
|
|| (uri.Scheme != "http" && uri.Scheme != "https"))
|
|
{
|
|
error = $"The key 'server' should be an URI starting by http:// or https://";
|
|
return false;
|
|
}
|
|
parts = uri.UserInfo.Split(':');
|
|
if (!string.IsNullOrEmpty(uri.UserInfo) && parts.Length == 2)
|
|
{
|
|
result.Username = parts[0];
|
|
result.Password = parts[1];
|
|
}
|
|
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
|
|
|
|
var macaroon = Take(keyValues, "macaroon");
|
|
if (macaroon != null)
|
|
{
|
|
try
|
|
{
|
|
result.Macaroon = Encoder.DecodeData(macaroon);
|
|
}
|
|
catch
|
|
{
|
|
error = $"The key 'macaroon' format should be in hex";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
var macaroonFilePath = Take(keyValues, "macaroonfilepath");
|
|
if (macaroonFilePath != null)
|
|
{
|
|
if(macaroon != null)
|
|
{
|
|
error = $"The key 'macaroon' is already specified";
|
|
return false;
|
|
}
|
|
if(!macaroonFilePath.EndsWith(".macaroon", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
error = $"The key 'macaroonfilepath' should point to a .macaroon file";
|
|
return false;
|
|
}
|
|
result.MacaroonFilePath = macaroonFilePath;
|
|
}
|
|
|
|
string securitySet = null;
|
|
var certthumbprint = Take(keyValues, "certthumbprint");
|
|
if (certthumbprint != null)
|
|
{
|
|
try
|
|
{
|
|
var bytes = Encoders.Hex.DecodeData(certthumbprint.Replace(":", string.Empty, StringComparison.OrdinalIgnoreCase));
|
|
if (bytes.Length != 32)
|
|
{
|
|
error = $"The key 'certthumbprint' has invalid length: it should be the SHA256 of the PEM format of the certificate (32 bytes)";
|
|
return false;
|
|
}
|
|
result.CertificateThumbprint = bytes;
|
|
}
|
|
catch
|
|
{
|
|
error = $"The key 'certthumbprint' has invalid format: it should be the SHA256 of the PEM format of the certificate";
|
|
return false;
|
|
}
|
|
securitySet = "certthumbprint";
|
|
}
|
|
|
|
var allowinsecureStr = Take(keyValues, "allowinsecure");
|
|
|
|
if (allowinsecureStr != null)
|
|
{
|
|
var allowedValues = new[] { "true", "false" };
|
|
if (!allowedValues.Any(v => v.Equals(allowinsecureStr, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
error = $"The key 'allowinsecure' should be true or false";
|
|
return false;
|
|
}
|
|
|
|
bool allowInsecure = allowinsecureStr.Equals("true", StringComparison.OrdinalIgnoreCase);
|
|
if (securitySet != null && allowInsecure)
|
|
{
|
|
error = $"The key 'allowinsecure' conflict with '{securitySet}'";
|
|
return false;
|
|
}
|
|
result.AllowInsecure = allowInsecure;
|
|
}
|
|
|
|
if (!result.AllowInsecure && result.BaseUri.Scheme == "http")
|
|
{
|
|
error = $"The key 'allowinsecure' is false, but server's Uri is not using https";
|
|
return false;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
throw new NotSupportedException(connectionType.ToString());
|
|
}
|
|
|
|
if (keyValues.Count != 0)
|
|
{
|
|
error = $"Unknown keys ({String.Join(", ", keyValues.Select(k => k.Key).ToArray())})";
|
|
return false;
|
|
}
|
|
|
|
connectionString = result;
|
|
return true;
|
|
}
|
|
private static string Take(Dictionary<string, string> keyValues, string key)
|
|
{
|
|
if (keyValues.TryGetValue(key, out var v))
|
|
keyValues.Remove(key);
|
|
return v;
|
|
}
|
|
|
|
private static bool TryParseLegacy(string str, out LightningConnectionString connectionString, out string error)
|
|
{
|
|
if (str.StartsWith('/'))
|
|
str = "unix:" + str;
|
|
var result = new LightningConnectionString();
|
|
connectionString = null;
|
|
error = null;
|
|
|
|
Uri uri;
|
|
if (!Uri.TryCreate(str, UriKind.Absolute, out uri))
|
|
{
|
|
error = "Invalid URL";
|
|
return false;
|
|
}
|
|
|
|
var supportedDomains = new string[] { "unix", "tcp", "http", "https" };
|
|
if (!supportedDomains.Contains(uri.Scheme))
|
|
{
|
|
var protocols = String.Join(",", supportedDomains);
|
|
error = $"The url support the following protocols {protocols}";
|
|
return false;
|
|
}
|
|
if (uri.Scheme == "unix")
|
|
{
|
|
str = uri.AbsoluteUri.Substring("unix:".Length);
|
|
while (str.Length >= 1 && str[0] == '/')
|
|
{
|
|
str = str.Substring(1);
|
|
}
|
|
uri = new Uri("unix://" + str, UriKind.Absolute);
|
|
result.ConnectionType = LightningConnectionType.CLightning;
|
|
}
|
|
|
|
if (uri.Scheme == "tcp")
|
|
result.ConnectionType = LightningConnectionType.CLightning;
|
|
|
|
if (uri.Scheme == "http" || uri.Scheme == "https")
|
|
{
|
|
var parts = uri.UserInfo.Split(':');
|
|
if (string.IsNullOrEmpty(uri.UserInfo) || parts.Length != 2)
|
|
{
|
|
error = "The url is missing user and password";
|
|
return false;
|
|
}
|
|
result.Username = parts[0];
|
|
result.Password = parts[1];
|
|
result.ConnectionType = LightningConnectionType.Charge;
|
|
}
|
|
else if (!string.IsNullOrEmpty(uri.UserInfo))
|
|
{
|
|
error = "The url should not have user information";
|
|
return false;
|
|
}
|
|
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
|
|
result.IsLegacy = true;
|
|
connectionString = result;
|
|
return true;
|
|
}
|
|
|
|
public LightningConnectionString()
|
|
{
|
|
|
|
}
|
|
|
|
public string Username { get; set; }
|
|
public string Password { get; set; }
|
|
public Uri BaseUri { get; set; }
|
|
public bool IsLegacy { get; private set; }
|
|
|
|
public LightningConnectionType ConnectionType
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
public byte[] Macaroon { get; set; }
|
|
public string MacaroonFilePath { get; set; }
|
|
public byte[] CertificateThumbprint { get; set; }
|
|
public bool AllowInsecure { get; set; }
|
|
|
|
public Uri ToUri(bool withCredentials)
|
|
{
|
|
if (withCredentials)
|
|
{
|
|
return new UriBuilder(BaseUri) { UserName = Username ?? "", Password = Password ?? "" }.Uri;
|
|
}
|
|
else
|
|
{
|
|
return BaseUri;
|
|
}
|
|
}
|
|
static NBitcoin.DataEncoders.DataEncoder Encoder = NBitcoin.DataEncoders.Encoders.Hex;
|
|
public override string ToString()
|
|
{
|
|
var type = typeMappingReverse[ConnectionType];
|
|
StringBuilder builder = new StringBuilder();
|
|
builder.Append($"type={type}");
|
|
switch (ConnectionType)
|
|
{
|
|
case LightningConnectionType.Charge:
|
|
if (Username == null || Username == "api-token")
|
|
{
|
|
builder.Append($";server={BaseUri};api-token={Password}");
|
|
}
|
|
else
|
|
{
|
|
builder.Append($";server={ToUri(true)}");
|
|
}
|
|
break;
|
|
case LightningConnectionType.CLightning:
|
|
builder.Append($";server={BaseUri}");
|
|
break;
|
|
case LightningConnectionType.LndREST:
|
|
if (Username == null)
|
|
{
|
|
builder.Append($";server={BaseUri}");
|
|
}
|
|
else
|
|
{
|
|
builder.Append($";server={ToUri(true)}");
|
|
}
|
|
if (Macaroon != null)
|
|
{
|
|
builder.Append($";macaroon={Encoder.EncodeData(Macaroon)}");
|
|
}
|
|
if (MacaroonFilePath != null)
|
|
{
|
|
builder.Append($";macaroonfilepath={MacaroonFilePath}");
|
|
}
|
|
if (CertificateThumbprint != null)
|
|
{
|
|
builder.Append($";certthumbprint={Encoders.Hex.EncodeData(CertificateThumbprint)}");
|
|
}
|
|
if (AllowInsecure)
|
|
{
|
|
builder.Append($";allowinsecure=true");
|
|
}
|
|
break;
|
|
default:
|
|
throw new NotSupportedException(type);
|
|
}
|
|
return builder.ToString();
|
|
}
|
|
}
|
|
}
|