Compare commits

...

14 Commits

23 changed files with 472 additions and 38 deletions

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.2.40</Version>
<Version>1.0.2.43</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>
@ -111,6 +111,7 @@
<ItemGroup>
<Folder Include="Build\" />
<Folder Include="wwwroot\vendor\clipboard.js\" />
<Folder Include="wwwroot\vendor\highlightjs\" />
</ItemGroup>
<ItemGroup>

View File

@ -82,7 +82,9 @@ namespace BTCPayServer.Configuration
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
$"If you have a lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
$"If you have a lightning charge server: 'type=charge;server=https://charge.example.com;api-token=yourapitoken'" + Environment.NewLine +
$"If you have a lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaron=abf239...;certthumbprint=2abdf302...'");
$"If you have a lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
$" lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
error);
}
if(connectionString.IsLegacy)
{

View File

@ -175,6 +175,7 @@ namespace BTCPayServer.Controllers
[Route("invoice")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
[XFrameOptionsAttribute(null)]
[ReferrerPolicyAttribute("origin")]
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null)
{
//Keep compatibility with Bitpay
@ -186,6 +187,20 @@ namespace BTCPayServer.Controllers
if (model == null)
return NotFound();
_CSP.Add(new ConsentSecurityPolicy("script-src", "'unsafe-eval'")); // Needed by Vue
if(!string.IsNullOrEmpty(model.CustomCSSLink) &&
Uri.TryCreate(model.CustomCSSLink, UriKind.Absolute, out var uri))
{
_CSP.Clear();
}
if (!string.IsNullOrEmpty(model.CustomLogoLink) &&
Uri.TryCreate(model.CustomLogoLink, UriKind.Absolute, out uri))
{
_CSP.Clear();
}
return View(nameof(Checkout), model);
}

View File

@ -41,12 +41,14 @@ using NBXplorer;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Security;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController : Controller
{
InvoiceRepository _InvoiceRepository;
ContentSecurityPolicies _CSP;
BTCPayRateProviderFactory _RateProvider;
StoreRepository _StoreRepository;
UserManager<ApplicationUser> _UserManager;
@ -64,6 +66,7 @@ namespace BTCPayServer.Controllers
StoreRepository storeRepository,
EventAggregator eventAggregator,
BTCPayWalletProvider walletProvider,
ContentSecurityPolicies csp,
BTCPayNetworkProvider networkProvider)
{
_ServiceProvider = serviceProvider;
@ -75,6 +78,7 @@ namespace BTCPayServer.Controllers
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
_WalletProvider = walletProvider;
_CSP = csp;
}

View File

@ -98,6 +98,26 @@ namespace BTCPayServer
return str + "/";
}
public static void SetHeaderOnStarting(this HttpResponse resp, string name, string value)
{
if (resp.HasStarted)
return;
resp.OnStarting(() =>
{
SetHeader(resp, name, value);
return Task.CompletedTask;
});
}
public static void SetHeader(this HttpResponse resp, string name, string value)
{
var existing = resp.Headers[name].FirstOrDefault();
if (existing != null && value == null)
resp.Headers.Remove(name);
else
resp.Headers[name] = value;
}
public static string GetAbsoluteRoot(this HttpRequest request)
{
return string.Concat(

View File

@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Security;
using Microsoft.AspNetCore.Mvc.Filters;
namespace BTCPayServer.Filters
{
public interface IContentSecurityPolicy : IFilterMetadata { }
public class ContentSecurityPolicyAttribute : Attribute, IActionFilter, IContentSecurityPolicy
{
public void OnActionExecuted(ActionExecutedContext context)
{
}
public bool AutoSelf { get; set; } = true;
public bool UnsafeInline { get; set; } = true;
public bool FixWebsocket { get; set; } = true;
public string FontSrc { get; set; } = null;
public string ImgSrc { get; set; } = null;
public string DefaultSrc { get; set; }
public string StyleSrc { get; set; }
public string ScriptSrc { get; set; }
public void OnActionExecuting(ActionExecutingContext context)
{
if (context.IsEffectivePolicy<IContentSecurityPolicy>(this))
{
var policies = context.HttpContext.RequestServices.GetService(typeof(ContentSecurityPolicies)) as ContentSecurityPolicies;
if (policies == null)
return;
if (DefaultSrc != null)
{
policies.Add(new ConsentSecurityPolicy("default-src", DefaultSrc));
}
if (UnsafeInline)
{
policies.Add(new ConsentSecurityPolicy("script-src", "'unsafe-inline'"));
}
if (!string.IsNullOrEmpty(FontSrc))
{
policies.Add(new ConsentSecurityPolicy("font-src", FontSrc));
}
if (!string.IsNullOrEmpty(ImgSrc))
{
policies.Add(new ConsentSecurityPolicy("img-src", ImgSrc));
}
if (!string.IsNullOrEmpty(StyleSrc))
{
policies.Add(new ConsentSecurityPolicy("style-src", StyleSrc));
}
if (!string.IsNullOrEmpty(ScriptSrc))
{
policies.Add(new ConsentSecurityPolicy("script-src", ScriptSrc));
}
if (FixWebsocket && AutoSelf) // Self does not match wss:// and ws:// :(
{
var request = context.HttpContext.Request;
var url = string.Concat(
request.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) ? "ws" : "wss",
"://",
request.Host.ToUriComponent(),
request.PathBase.ToUriComponent());
policies.Add(new ConsentSecurityPolicy("connect-src", url));
}
context.HttpContext.Response.OnStarting(() =>
{
if (!policies.HasRules)
return Task.CompletedTask;
if (AutoSelf)
{
bool hasSelf = false;
foreach (var group in policies.Rules.GroupBy(p => p.Name))
{
hasSelf = group.Any(g => g.Value.Contains("'self'", StringComparison.OrdinalIgnoreCase));
if (!hasSelf && !group.Any(g => g.Value.Contains("'none'", StringComparison.OrdinalIgnoreCase) ||
g.Value.Contains("*", StringComparison.OrdinalIgnoreCase)))
{
policies.Add(new ConsentSecurityPolicy(group.Key, "'self'"));
hasSelf = true;
}
if (hasSelf)
{
foreach (var authorized in policies.Authorized)
{
policies.Add(new ConsentSecurityPolicy(group.Key, authorized));
}
}
}
}
context.HttpContext.Response.SetHeader("Content-Security-Policy", policies.ToString());
return Task.CompletedTask;
});
}
}
}
}

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;
namespace BTCPayServer.Filters
{
public interface IReferrerPolicy : IFilterMetadata { }
public class ReferrerPolicyAttribute : Attribute, IActionFilter
{
public ReferrerPolicyAttribute(string value)
{
Value = value;
}
public string Value { get; set; }
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
if (context.IsEffectivePolicy<ReferrerPolicyAttribute>(this))
{
context.HttpContext.Response.SetHeaderOnStarting("Referrer-Policy", Value);
}
}
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;
namespace BTCPayServer.Filters
{
public class XContentTypeOptionsAttribute : Attribute, IActionFilter
{
public XContentTypeOptionsAttribute(string value)
{
Value = value;
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
public string Value { get; set; }
public void OnActionExecuting(ActionExecutingContext context)
{
context.HttpContext.Response.SetHeaderOnStarting("X-Content-Type-Options", Value);
}
}
}

View File

@ -23,11 +23,7 @@ namespace BTCPayServer.Filters
public void OnActionExecuting(ActionExecutingContext context)
{
var existing = context.HttpContext.Response.Headers["x-frame-options"].FirstOrDefault();
if (existing != null && Value == null)
context.HttpContext.Response.Headers.Remove("x-frame-options");
else
context.HttpContext.Response.Headers["x-frame-options"] = Value;
context.HttpContext.Response.SetHeaderOnStarting("X-Frame-Options", Value);
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
namespace BTCPayServer.Filters
{
public class XXSSProtectionAttribute : Attribute, IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
context.HttpContext.Response.SetHeaderOnStarting("X-XSS-Protection", "1; mode=block");
}
}
}

View File

@ -11,6 +11,8 @@ using NBXplorer.Models;
using System.Collections.Concurrent;
using BTCPayServer.Events;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc.Filters;
using BTCPayServer.Security;
namespace BTCPayServer.HostedServices
{
@ -50,6 +52,33 @@ namespace BTCPayServer.HostedServices
}
}
public class ContentSecurityPolicyCssThemeManager : Attribute, IActionFilter, IOrderedFilter
{
public int Order => 1001;
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
var manager = context.HttpContext.RequestServices.GetService(typeof(CssThemeManager)) as CssThemeManager;
var policies = context.HttpContext.RequestServices.GetService(typeof(ContentSecurityPolicies)) as ContentSecurityPolicies;
if (manager != null && policies != null)
{
if(manager.CreativeStartUri != null && Uri.TryCreate(manager.CreativeStartUri, UriKind.Absolute, out var uri))
{
policies.Clear();
}
if (manager.BootstrapUri != null && Uri.TryCreate(manager.BootstrapUri, UriKind.Absolute, out uri))
{
policies.Clear();
}
}
}
}
public class CssThemeManagerHostedService : BaseAsyncService
{
private SettingsRepository _SettingsRepository;

View File

@ -104,6 +104,7 @@ namespace BTCPayServer.Hosting
});
services.AddSingleton<CssThemeManager>();
services.Configure<MvcOptions>((o) => { o.Filters.Add(new ContentSecurityPolicyCssThemeManager()); });
services.AddSingleton<IHostedService, CssThemeManagerHostedService>();
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>();

View File

@ -39,6 +39,7 @@ using Microsoft.AspNetCore.Mvc.Cors.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using System.Net;
using Meziantou.AspNetCore.BundleTagHelpers;
using BTCPayServer.Security;
namespace BTCPayServer.Hosting
{
@ -79,8 +80,19 @@ namespace BTCPayServer.Hosting
services.AddMvc(o =>
{
o.Filters.Add(new XFrameOptionsAttribute("DENY"));
o.Filters.Add(new XContentTypeOptionsAttribute("nosniff"));
o.Filters.Add(new XXSSProtectionAttribute());
o.Filters.Add(new ReferrerPolicyAttribute("same-origin"));
//o.Filters.Add(new ContentSecurityPolicyAttribute()
//{
// FontSrc = "'self' https://fonts.gstatic.com/",
// ImgSrc = "'self' data:",
// DefaultSrc = "'none'",
// StyleSrc = "'self' 'unsafe-inline'",
// ScriptSrc = "'self' 'unsafe-inline'"
//});
});
services.TryAddScoped<ContentSecurityPolicies>();
services.Configure<IdentityOptions>(options =>
{
options.Password.RequireDigit = false;

View File

@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Security
{
public class ConsentSecurityPolicy
{
public ConsentSecurityPolicy(string name, string value)
{
_Value = value;
_Name = name;
}
private readonly string _Name;
public string Name
{
get
{
return _Name;
}
}
private readonly string _Value;
public string Value
{
get
{
return _Value;
}
}
public override bool Equals(object obj)
{
ConsentSecurityPolicy item = obj as ConsentSecurityPolicy;
if (item == null)
return false;
return GetHashCode().Equals(item.GetHashCode());
}
public static bool operator ==(ConsentSecurityPolicy a, ConsentSecurityPolicy b)
{
if (System.Object.ReferenceEquals(a, b))
return true;
if (((object)a == null) || ((object)b == null))
return false;
return a.GetHashCode() == b.GetHashCode();
}
public static bool operator !=(ConsentSecurityPolicy a, ConsentSecurityPolicy b)
{
return !(a == b);
}
public override int GetHashCode()
{
return HashCode.Combine(Name, Value);
}
}
public class ContentSecurityPolicies
{
public ContentSecurityPolicies()
{
}
HashSet<ConsentSecurityPolicy> _Policies = new HashSet<ConsentSecurityPolicy>();
public void Add(ConsentSecurityPolicy policy)
{
if (_Policies.Any(p => p.Name == policy.Name && p.Value == policy.Name))
return;
_Policies.Add(policy);
}
public IEnumerable<ConsentSecurityPolicy> Rules => _Policies;
public bool HasRules => _Policies.Count != 0;
public override string ToString()
{
StringBuilder value = new StringBuilder();
bool firstGroup = true;
foreach(var group in Rules.GroupBy(r => r.Name))
{
if (!firstGroup)
{
value.Append(';');
}
List<string> values = new List<string>();
values.Add(group.Key);
foreach (var v in group)
{
values.Add(v.Value);
}
foreach(var i in authorized)
{
values.Add(i);
}
value.Append(String.Join(" ", values.OfType<object>().ToArray()));
firstGroup = false;
}
return value.ToString();
}
internal void Clear()
{
authorized.Clear();
_Policies.Clear();
}
HashSet<string> authorized = new HashSet<string>();
internal void AddAllAuthorized(string v)
{
authorized.Add(v);
}
public IEnumerable<string> Authorized => authorized;
}
}

View File

@ -7,13 +7,17 @@ using System.Threading.Tasks;
using System.Text;
using NBXplorer;
using NBitcoin;
using Microsoft.AspNetCore.Http;
namespace BTCPayServer.Services
{
public class BTCPayServerEnvironment
{
public BTCPayServerEnvironment(IHostingEnvironment env, BTCPayNetworkProvider provider)
public BTCPayServerEnvironment(IHostingEnvironment env, BTCPayNetworkProvider provider, IHttpContextAccessor httpContext)
{
ExpectedHost = httpContext.HttpContext.Request.Host.Value;
ExpectedDomain = httpContext.HttpContext.Request.Host.Host;
ExpectedProtocol = httpContext.HttpContext.Request.Scheme;
Version = typeof(BTCPayServerEnvironment).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>().Version;
#if DEBUG
Build = "Debug";
@ -27,6 +31,11 @@ namespace BTCPayServer.Services
{
get; set;
}
public string ExpectedDomain { get; set; }
public string ExpectedHost { get; set; }
public string ExpectedProtocol { get; set; }
public NetworkType NetworkType { get; set; }
public string Version
{

View File

@ -74,8 +74,8 @@
</section>
@section Scripts {
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
<link rel="stylesheet" href="~/vendor/highlightjs/default.min.css">
<script src="~/vendor/highlightjs/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
}

View File

@ -57,11 +57,14 @@
<div class="container text-center">
<h2>Video tutorials</h2>
<div class="row">
<div class="col-md-6 text-center">
<iframe width="400" height="225" src="https://www.youtube.com/embed/xh3Eac66qc4" frameborder="0" allowfullscreen></iframe>
<div class="col-md-4 text-center">
</div>
<div class="col-md-4 text-center">
<a href="https://www.youtube.com/channel/UCpG9WL6TJuoNfFVkaDMp9ug" target="_blank">
<img src="~/img/youtube.png" height="225" width="400" />
</a>
</div>
<div class="col-md-6 text-center">
<iframe width="400" height="225" src="https://www.youtube.com/embed/Xo_vApXTZBU" frameborder="0" allowfullscreen></iframe>
</div>
</div>
</div>

View File

@ -78,9 +78,19 @@
</ul>
</div>
<div id="badUrl" class="alert alert-danger alert-dismissible" style="display:none; position:absolute; top:75px;" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span>BTCPay is expecting you to access this website from <b>@(env.ExpectedProtocol)://@(env.ExpectedHost)/</b>. If you want to change this expectation:</span>
<ul>
<li>Either starts BTCPay with <b>--externalurl @(env.ExpectedProtocol)://@(env.ExpectedHost)/</b></li>
<li>Or, if using docker-compose deployment, setting environment variable <b>BTCPAY_PROTOCOL=@(env.ExpectedProtocol)</b> and <b>BTCPAY_HOST=@(env.ExpectedDomain)</b></li>
</ul>
</div>
</div>
</nav>
@RenderBody()
<footer class="siteFooter bg-dark text-white">
@ -93,6 +103,14 @@
}
@RenderSection("Scripts", required: false)
<script type="text/javascript">
var expectedDomain = @Html.Raw(Json.Serialize(env.ExpectedHost));
var expectedProtocol = @Html.Raw(Json.Serialize(env.ExpectedProtocol));
if (window.location.host != expectedDomain || window.location.protocol != expectedProtocol + ":") {
document.getElementById("badUrl").style.display = "block";
}
</script>
</body>
</html>

View File

@ -7,33 +7,49 @@
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="alert alert-warning alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<p>
<span>A connection to a lightning charge node or clightning unix socket is required to generate lightning network enabled invoices. <br /></span>
<span>This is experimental and not advised for production so keep in mind:</span>
</p>
<ul>
<li>You might lose your money</li>
<li>The devs of BTCPay Server don't know what they are doing and won't be able to help you if shit hit the fan</li>
<li>You approve being #reckless and being the sole responsible party for your loss</li>
<li>BTCPay Server relies on a <a href="https://github.com/ElementsProject/lightning-charge">Lightning Charge</a> node or CLightning unix socket</li>
<li>If you have no idea what above mean, search by yourself</li>
<li>If you still have no idea how to use lightning, give up for now, we'll make it easier later</li>
</ul>
</div>
<div class="row">
<div class="col-md-8">
<div class="col-md-10">
<form method="post">
<div class="form-group">
<h5>Lightning node url</h5>
<span>This URL should point to an installed lightning charge server for @Model.CryptoCode</span>
<p>This connection string encapsulates the necessary information BTCPay needs to connect to your lightning node, we currently support:</p>
<ul>
<li><code>clightning</code> via TCP or unix domain socket connection</li>
<li><code>lightning charge</code> via HTTPS</li>
<li><code>lnd</code> via the REST proxy</li>
</ul>
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Examples</th>
</tr>
</thead>
<tbody>
<tr>
<th class="small"><b>type=</b>clightning;<b>server=</b>unix://root/.lightning/lightning-rpc</th>
</tr>
<tr>
<th class="small"><b>type=</b>clightning;<b>server=</b>tcp://1.1.1.1:27743/</th>
</tr>
<tr>
<th class="small"><b>type=</b>lnd-rest;<b>server=</b>http://mylnd:8080/;<b>macaroonfilepath=</b>/root/.lnd/admin.macaroon;<b>allowinsecure=</b>true</th>
</tr>
<tr>
<th class="small"><b>type=</b>lnd-rest;<b>server=</b>https://mylnd:8080/;<b>macaroon=</b>abef263adfe...</th>
</tr>
<tr>
<th class="small"><b>type=</b>lnd-rest;<b>server=</b>https://mylnd:8080/;<b>macaroon=</b>abef263adfe...;<b>certthumbprint=</b>abef263adfe...</th>
</tr>
<tr>
<th class="small"><b>type=</b>charge;<b>server=</b>https://charge:8080/;<b>api-token=</b>myapitoken...</th>
</tr>
</tbody>
</table>
<p>Note that the <code>certthumbprint</code> to connect to your LND node can be obtained through this command line:</p>
<p><pre><code>openssl x509 -noout -fingerprint -sha256 -inform pem -in /root/.lnd/tls.cert</code></pre></p>
<p>You can omit <code>certthumprint</code> if you the certificate is trusted by your machine</p>
<p>You can set <code>allowinsecure</code> to <code>true</code> if your LND REST server is using HTTP or HTTPS with an untrusted certificate which you don't know the <code>certthumprint</code></p>
</div>
<div class="form-group">
<label asp-for="ConnectionString"></label>

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -2902,7 +2902,7 @@ tbody.collapse.show {
border-color: rgba(0, 0, 0, 0.1); }
.navbar-light .navbar-toggler-icon {
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E"); }
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E"); }
.navbar-light .navbar-text {
color: rgba(0, 0, 0, 0.5); }

View File

@ -0,0 +1 @@
.hljs{display:block;overflow-x:auto;padding:0.5em;background:#F0F0F0}.hljs,.hljs-subst{color:#444}.hljs-comment{color:#888888}.hljs-keyword,.hljs-attribute,.hljs-selector-tag,.hljs-meta-keyword,.hljs-doctag,.hljs-name{font-weight:bold}.hljs-type,.hljs-string,.hljs-number,.hljs-selector-id,.hljs-selector-class,.hljs-quote,.hljs-template-tag,.hljs-deletion{color:#880000}.hljs-title,.hljs-section{color:#880000;font-weight:bold}.hljs-regexp,.hljs-symbol,.hljs-variable,.hljs-template-variable,.hljs-link,.hljs-selector-attr,.hljs-selector-pseudo{color:#BC6060}.hljs-literal{color:#78A960}.hljs-built_in,.hljs-bullet,.hljs-code,.hljs-addition{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta-string{color:#4d99bf}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold}

File diff suppressed because one or more lines are too long