App.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. /*
  2. Technitium DNS Server
  3. Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com)
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. */
  15. using DnsServerCore.ApplicationCommon;
  16. using Microsoft.AspNetCore.Builder;
  17. using Microsoft.AspNetCore.Hosting;
  18. using Microsoft.AspNetCore.Http;
  19. using Microsoft.AspNetCore.Server.Kestrel.Core;
  20. using Microsoft.AspNetCore.StaticFiles;
  21. using Microsoft.Extensions.FileProviders;
  22. using Microsoft.Extensions.Logging;
  23. using System;
  24. using System.Collections.Generic;
  25. using System.IO;
  26. using System.Net;
  27. using System.Net.Security;
  28. using System.Security.Cryptography;
  29. using System.Security.Cryptography.X509Certificates;
  30. using System.Text;
  31. using System.Text.Json;
  32. using System.Threading;
  33. using System.Threading.Tasks;
  34. using TechnitiumLibrary;
  35. namespace BlockPage
  36. {
  37. public sealed class App : IDnsApplication
  38. {
  39. #region variables
  40. IReadOnlyDictionary<string, WebServer> _webServers;
  41. #endregion
  42. #region IDisposable
  43. bool _disposed;
  44. public void Dispose()
  45. {
  46. if (_disposed)
  47. return;
  48. StopAllWebServersAsync().Sync();
  49. _disposed = true;
  50. }
  51. #endregion
  52. #region private
  53. private async Task StopAllWebServersAsync()
  54. {
  55. if (_webServers is not null)
  56. {
  57. foreach (KeyValuePair<string, WebServer> webServerEntry in _webServers)
  58. await webServerEntry.Value.DisposeAsync();
  59. _webServers = null;
  60. }
  61. }
  62. #endregion
  63. #region public
  64. public async Task InitializeAsync(IDnsServer dnsServer, string config)
  65. {
  66. using JsonDocument jsonDocument = JsonDocument.Parse(config);
  67. JsonElement jsonConfig = jsonDocument.RootElement;
  68. await StopAllWebServersAsync();
  69. Dictionary<string, WebServer> webServers = new Dictionary<string, WebServer>(3);
  70. _webServers = webServers;
  71. if (jsonConfig.ValueKind == JsonValueKind.Array)
  72. {
  73. foreach (JsonElement jsonWebServerConfig in jsonConfig.EnumerateArray())
  74. {
  75. string name = jsonWebServerConfig.GetPropertyValue("name", "default");
  76. if (!webServers.TryGetValue(name, out WebServer webServer))
  77. {
  78. webServer = new WebServer(dnsServer, name);
  79. if (!webServers.TryAdd(webServer.Name, webServer))
  80. throw new InvalidOperationException("Failed to update web server config. Please try again.");
  81. }
  82. await webServer.InitializeAsync(jsonWebServerConfig);
  83. }
  84. }
  85. else
  86. {
  87. WebServer webServer = new WebServer(dnsServer, "default");
  88. webServers.Add(webServer.Name, webServer);
  89. await webServer.InitializeAsync(jsonConfig);
  90. if (!jsonConfig.TryGetProperty("webServerUseSelfSignedTlsCertificate", out _))
  91. config = config.Replace("\"webServerTlsCertificateFilePath\"", "\"webServerUseSelfSignedTlsCertificate\": true,\r\n \"webServerTlsCertificateFilePath\"");
  92. if (!jsonConfig.TryGetProperty("enableWebServer", out _))
  93. config = config.Replace("\"webServerLocalAddresses\"", "\"enableWebServer\": true,\r\n \"webServerLocalAddresses\"");
  94. if (!jsonConfig.TryGetProperty("name", out _))
  95. config = config.Replace("\"enableWebServer\"", "\"name\": \"default\",\r\n \"enableWebServer\"");
  96. config = "[\r\n " + config.Replace("\n", "\n ").TrimEnd() + "\r\n]";
  97. await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config);
  98. }
  99. }
  100. #endregion
  101. #region properties
  102. public string Description
  103. { get { return "Serves a block page from a built-in web server that can be displayed to the end user when a website is blocked by the DNS server.\n\nNote: You need to manually set the Blocking Type as Custom Address in the blocking settings and configure the current server's IP address as Custom Blocking Addresses for the block page to be served to the users. Use a PKCS #12 certificate (.pfx or .p12) for enabling HTTPS support. Enabling HTTPS support will show certificate error to the user which is expected and the user will have to proceed ignoring the certificate error to be able to see the block page."; } }
  104. #endregion
  105. class WebServer : IAsyncDisposable
  106. {
  107. #region variables
  108. readonly IDnsServer _dnsServer;
  109. readonly string _name;
  110. IReadOnlyList<IPAddress> _webServerLocalAddresses = Array.Empty<IPAddress>();
  111. bool _webServerUseSelfSignedTlsCertificate;
  112. string _webServerTlsCertificateFilePath;
  113. string _webServerTlsCertificatePassword;
  114. string _webServerRootPath;
  115. bool _serveBlockPageFromWebServerRoot;
  116. byte[] _blockPageContent;
  117. WebApplication _webServer;
  118. X509Certificate2Collection _webServerTlsCertificateCollection;
  119. SslServerAuthenticationOptions _sslServerAuthenticationOptions;
  120. DateTime _webServerTlsCertificateLastModifiedOn;
  121. Timer _tlsCertificateUpdateTimer;
  122. const int TLS_CERTIFICATE_UPDATE_TIMER_INITIAL_INTERVAL = 60000;
  123. const int TLS_CERTIFICATE_UPDATE_TIMER_INTERVAL = 60000;
  124. #endregion
  125. #region constructor
  126. public WebServer(IDnsServer dnsServer, string name)
  127. {
  128. _dnsServer = dnsServer;
  129. _name = name;
  130. }
  131. #endregion
  132. #region IDisposable
  133. bool _disposed;
  134. public async ValueTask DisposeAsync()
  135. {
  136. if (_disposed)
  137. return;
  138. await StopTlsCertificateUpdateTimerAsync();
  139. await StopWebServerAsync();
  140. _disposed = true;
  141. }
  142. #endregion
  143. #region private
  144. private async Task StartWebServerAsync()
  145. {
  146. WebApplicationBuilder builder = WebApplication.CreateBuilder();
  147. if (_serveBlockPageFromWebServerRoot)
  148. {
  149. builder.Environment.ContentRootFileProvider = new PhysicalFileProvider(_dnsServer.ApplicationFolder)
  150. {
  151. UseActivePolling = true,
  152. UsePollingFileWatcher = true
  153. };
  154. builder.Environment.WebRootFileProvider = new PhysicalFileProvider(_webServerRootPath)
  155. {
  156. UseActivePolling = true,
  157. UsePollingFileWatcher = true
  158. };
  159. }
  160. builder.WebHost.ConfigureKestrel(delegate (WebHostBuilderContext context, KestrelServerOptions serverOptions)
  161. {
  162. //http
  163. foreach (IPAddress webServiceLocalAddress in _webServerLocalAddresses)
  164. serverOptions.Listen(webServiceLocalAddress, 80);
  165. //https
  166. if (_webServerTlsCertificateCollection is not null)
  167. {
  168. foreach (IPAddress webServiceLocalAddress in _webServerLocalAddresses)
  169. {
  170. serverOptions.Listen(webServiceLocalAddress, 443, delegate (ListenOptions listenOptions)
  171. {
  172. listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
  173. listenOptions.UseHttps(delegate (SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken)
  174. {
  175. return ValueTask.FromResult(_sslServerAuthenticationOptions);
  176. }, null);
  177. });
  178. }
  179. }
  180. serverOptions.AddServerHeader = false;
  181. serverOptions.Limits.MaxRequestBodySize = int.MaxValue;
  182. });
  183. builder.Logging.ClearProviders();
  184. _webServer = builder.Build();
  185. _webServer.UseDefaultFiles();
  186. _webServer.UseStaticFiles(new StaticFileOptions()
  187. {
  188. OnPrepareResponse = delegate (StaticFileResponseContext ctx)
  189. {
  190. ctx.Context.Response.Headers["X-Robots-Tag"] = "noindex, nofollow";
  191. ctx.Context.Response.Headers.CacheControl = "private, max-age=300";
  192. },
  193. ServeUnknownFileTypes = true
  194. });
  195. if (_serveBlockPageFromWebServerRoot)
  196. _webServer.Use(RedirectToDefaultPageAsync);
  197. else
  198. _webServer.Use(ServeDefaultPageAsync);
  199. try
  200. {
  201. await _webServer.StartAsync();
  202. foreach (IPAddress webServiceLocalAddress in _webServerLocalAddresses)
  203. {
  204. _dnsServer.WriteLog("Web server '" + _name + "' was bound successfully: " + new IPEndPoint(webServiceLocalAddress, 80).ToString());
  205. if (_webServerTlsCertificateCollection is not null)
  206. _dnsServer.WriteLog("Web server '" + _name + "' was bound successfully: " + new IPEndPoint(webServiceLocalAddress, 443).ToString());
  207. }
  208. }
  209. catch (Exception ex)
  210. {
  211. await StopWebServerAsync();
  212. foreach (IPAddress webServiceLocalAddress in _webServerLocalAddresses)
  213. {
  214. _dnsServer.WriteLog("Web server '" + _name + "' failed to bind: " + new IPEndPoint(webServiceLocalAddress, 80).ToString());
  215. if (_webServerTlsCertificateCollection is not null)
  216. _dnsServer.WriteLog("Web server '" + _name + "' failed to bind: " + new IPEndPoint(webServiceLocalAddress, 443).ToString());
  217. }
  218. _dnsServer.WriteLog(ex);
  219. }
  220. }
  221. private async Task StopWebServerAsync()
  222. {
  223. if (_webServer is not null)
  224. {
  225. await _webServer.DisposeAsync();
  226. _webServer = null;
  227. }
  228. }
  229. private void LoadWebServiceTlsCertificate(string webServerTlsCertificateFilePath, string webServerTlsCertificatePassword)
  230. {
  231. FileInfo fileInfo = new FileInfo(webServerTlsCertificateFilePath);
  232. if (!fileInfo.Exists)
  233. throw new ArgumentException("Web server '" + _name + "' TLS certificate file does not exists: " + webServerTlsCertificateFilePath);
  234. switch (Path.GetExtension(webServerTlsCertificateFilePath).ToLowerInvariant())
  235. {
  236. case ".pfx":
  237. case ".p12":
  238. break;
  239. default:
  240. throw new ArgumentException("Web server '" + _name + "' TLS certificate file must be PKCS #12 formatted with .pfx or .p12 extension: " + webServerTlsCertificateFilePath);
  241. }
  242. _webServerTlsCertificateCollection = new X509Certificate2Collection();
  243. _webServerTlsCertificateCollection.Import(webServerTlsCertificateFilePath, webServerTlsCertificatePassword, X509KeyStorageFlags.PersistKeySet);
  244. X509Certificate2 serverCertificate = null;
  245. foreach (X509Certificate2 certificate in _webServerTlsCertificateCollection)
  246. {
  247. if (certificate.HasPrivateKey)
  248. {
  249. serverCertificate = certificate;
  250. break;
  251. }
  252. }
  253. if (serverCertificate is null)
  254. throw new ArgumentException("Web server '" + _name + "' TLS certificate file must contain a certificate with private key.");
  255. _sslServerAuthenticationOptions = new SslServerAuthenticationOptions()
  256. {
  257. ServerCertificateContext = SslStreamCertificateContext.Create(serverCertificate, _webServerTlsCertificateCollection, false)
  258. };
  259. _webServerTlsCertificateLastModifiedOn = fileInfo.LastWriteTimeUtc;
  260. _dnsServer.WriteLog("Web server '" + _name + "' TLS certificate was loaded: " + webServerTlsCertificateFilePath);
  261. }
  262. private void StartTlsCertificateUpdateTimer()
  263. {
  264. if (_tlsCertificateUpdateTimer is null)
  265. {
  266. _tlsCertificateUpdateTimer = new Timer(delegate (object state)
  267. {
  268. if (!string.IsNullOrEmpty(_webServerTlsCertificateFilePath))
  269. {
  270. try
  271. {
  272. FileInfo fileInfo = new FileInfo(_webServerTlsCertificateFilePath);
  273. if (fileInfo.Exists && (fileInfo.LastWriteTimeUtc != _webServerTlsCertificateLastModifiedOn))
  274. LoadWebServiceTlsCertificate(_webServerTlsCertificateFilePath, _webServerTlsCertificatePassword);
  275. }
  276. catch (Exception ex)
  277. {
  278. _dnsServer.WriteLog("Web server '" + _name + "' encountered an error while updating TLS Certificate: " + _webServerTlsCertificateFilePath + "\r\n" + ex.ToString());
  279. }
  280. }
  281. }, null, TLS_CERTIFICATE_UPDATE_TIMER_INITIAL_INTERVAL, TLS_CERTIFICATE_UPDATE_TIMER_INTERVAL);
  282. }
  283. }
  284. private async Task StopTlsCertificateUpdateTimerAsync()
  285. {
  286. if (_tlsCertificateUpdateTimer is not null)
  287. {
  288. await _tlsCertificateUpdateTimer.DisposeAsync();
  289. _tlsCertificateUpdateTimer = null;
  290. }
  291. }
  292. private Task RedirectToDefaultPageAsync(HttpContext context, RequestDelegate next)
  293. {
  294. context.Response.Redirect("/", false, true);
  295. return Task.CompletedTask;
  296. }
  297. private async Task ServeDefaultPageAsync(HttpContext context, RequestDelegate next)
  298. {
  299. HttpResponse response = context.Response;
  300. response.StatusCode = StatusCodes.Status200OK;
  301. response.ContentType = "text/html; charset=utf-8";
  302. response.ContentLength = _blockPageContent.Length;
  303. using (Stream s = context.Response.Body)
  304. {
  305. await s.WriteAsync(_blockPageContent);
  306. }
  307. }
  308. #endregion
  309. #region public
  310. public async Task InitializeAsync(JsonElement jsonWebServerConfig)
  311. {
  312. bool enableWebServer = jsonWebServerConfig.GetPropertyValue("enableWebServer", true);
  313. if (!enableWebServer)
  314. {
  315. await StopWebServerAsync();
  316. return;
  317. }
  318. _webServerLocalAddresses = jsonWebServerConfig.ReadArray("webServerLocalAddresses", IPAddress.Parse);
  319. if (jsonWebServerConfig.TryGetProperty("webServerUseSelfSignedTlsCertificate", out JsonElement jsonWebServerUseSelfSignedTlsCertificate))
  320. _webServerUseSelfSignedTlsCertificate = jsonWebServerUseSelfSignedTlsCertificate.GetBoolean();
  321. else
  322. _webServerUseSelfSignedTlsCertificate = true;
  323. _webServerTlsCertificateFilePath = jsonWebServerConfig.GetProperty("webServerTlsCertificateFilePath").GetString();
  324. _webServerTlsCertificatePassword = jsonWebServerConfig.GetProperty("webServerTlsCertificatePassword").GetString();
  325. _webServerRootPath = jsonWebServerConfig.GetProperty("webServerRootPath").GetString();
  326. if (!Path.IsPathRooted(_webServerRootPath))
  327. _webServerRootPath = Path.Combine(_dnsServer.ApplicationFolder, _webServerRootPath);
  328. _serveBlockPageFromWebServerRoot = jsonWebServerConfig.GetProperty("serveBlockPageFromWebServerRoot").GetBoolean();
  329. string blockPageTitle = jsonWebServerConfig.GetProperty("blockPageTitle").GetString();
  330. string blockPageHeading = jsonWebServerConfig.GetProperty("blockPageHeading").GetString();
  331. string blockPageMessage = jsonWebServerConfig.GetProperty("blockPageMessage").GetString();
  332. string blockPageContent = @"<html>
  333. <head>
  334. <title>" + (blockPageTitle is null ? "" : blockPageTitle) + @"</title>
  335. </head>
  336. <body>
  337. " + (blockPageHeading is null ? "" : " <h1>" + blockPageHeading + "</h1>") + @"
  338. " + (blockPageMessage is null ? "" : " <p>" + blockPageMessage + "</p>") + @"
  339. </body>
  340. </html>";
  341. _blockPageContent = Encoding.UTF8.GetBytes(blockPageContent);
  342. try
  343. {
  344. await StopWebServerAsync();
  345. string selfSignedCertificateFilePath = Path.Combine(_dnsServer.ApplicationFolder, "self-signed-cert.pfx");
  346. if (_webServerUseSelfSignedTlsCertificate)
  347. {
  348. string oldSelfSignedCertificateFilePath = Path.Combine(_dnsServer.ApplicationFolder, "cert.pfx");
  349. if (!oldSelfSignedCertificateFilePath.Equals(_webServerTlsCertificateFilePath, Environment.OSVersion.Platform == PlatformID.Win32NT ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal) && File.Exists(oldSelfSignedCertificateFilePath) && !File.Exists(selfSignedCertificateFilePath))
  350. File.Move(oldSelfSignedCertificateFilePath, selfSignedCertificateFilePath);
  351. if (!File.Exists(selfSignedCertificateFilePath))
  352. {
  353. RSA rsa = RSA.Create(2048);
  354. CertificateRequest req = new CertificateRequest("cn=" + _dnsServer.ServerDomain, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
  355. X509Certificate2 cert = req.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(5));
  356. await File.WriteAllBytesAsync(selfSignedCertificateFilePath, cert.Export(X509ContentType.Pkcs12, null as string));
  357. }
  358. }
  359. else
  360. {
  361. File.Delete(selfSignedCertificateFilePath);
  362. }
  363. if (string.IsNullOrEmpty(_webServerTlsCertificateFilePath))
  364. {
  365. await StopTlsCertificateUpdateTimerAsync();
  366. if (_webServerUseSelfSignedTlsCertificate)
  367. {
  368. LoadWebServiceTlsCertificate(selfSignedCertificateFilePath, null);
  369. }
  370. else
  371. {
  372. //disable HTTPS
  373. _webServerTlsCertificateCollection = null;
  374. }
  375. }
  376. else
  377. {
  378. LoadWebServiceTlsCertificate(_webServerTlsCertificateFilePath, _webServerTlsCertificatePassword);
  379. StartTlsCertificateUpdateTimer();
  380. }
  381. await StartWebServerAsync();
  382. }
  383. catch (Exception ex)
  384. {
  385. _dnsServer.WriteLog(ex);
  386. }
  387. }
  388. #endregion
  389. #region properties
  390. public string Name
  391. { get { return _name; } }
  392. #endregion
  393. }
  394. }
  395. }