App.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. /*
  2. Technitium DNS Server
  3. Copyright (C) 2023 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 System;
  17. using System.Collections.Generic;
  18. using System.IO;
  19. using System.Net;
  20. using System.Net.Security;
  21. using System.Net.Sockets;
  22. using System.Security.Cryptography;
  23. using System.Security.Cryptography.X509Certificates;
  24. using System.Text;
  25. using System.Text.Json;
  26. using System.Threading;
  27. using System.Threading.Tasks;
  28. using TechnitiumLibrary;
  29. using TechnitiumLibrary.Net;
  30. using TechnitiumLibrary.Net.Http;
  31. namespace BlockPage
  32. {
  33. public class App : IDnsApplication
  34. {
  35. #region enum
  36. enum ServiceState
  37. {
  38. Stopped = 0,
  39. Starting = 1,
  40. Running = 2,
  41. Stopping = 3
  42. }
  43. #endregion
  44. #region variables
  45. const int TCP_SEND_TIMEOUT = 10000;
  46. const int TCP_RECV_TIMEOUT = 10000;
  47. IDnsServer _dnsServer;
  48. IReadOnlyList<IPAddress> _webServerLocalAddresses = Array.Empty<IPAddress>();
  49. bool _webServerUseSelfSignedTlsCertificate;
  50. string _webServerTlsCertificateFilePath;
  51. string _webServerTlsCertificatePassword;
  52. string _webServerRootPath;
  53. bool _serveBlockPageFromWebServerRoot;
  54. byte[] _blockPageContent;
  55. readonly List<Socket> _httpListeners = new List<Socket>();
  56. readonly List<Socket> _httpsListeners = new List<Socket>();
  57. X509Certificate2 _webServerTlsCertificate;
  58. DateTime _webServerTlsCertificateLastModifiedOn;
  59. Timer _tlsCertificateUpdateTimer;
  60. const int TLS_CERTIFICATE_UPDATE_TIMER_INITIAL_INTERVAL = 60000;
  61. const int TLS_CERTIFICATE_UPDATE_TIMER_INTERVAL = 60000;
  62. volatile ServiceState _state = ServiceState.Stopped;
  63. #endregion
  64. #region IDisposable
  65. public void Dispose()
  66. {
  67. StopTlsCertificateUpdateTimer();
  68. StopWebServer();
  69. }
  70. #endregion
  71. #region private
  72. private void StartWebServer()
  73. {
  74. if (_state != ServiceState.Stopped)
  75. throw new InvalidOperationException("Web server is already running.");
  76. _state = ServiceState.Starting;
  77. //bind to local addresses
  78. foreach (IPAddress localAddress in _webServerLocalAddresses)
  79. {
  80. //bind to HTTP port 80
  81. {
  82. IPEndPoint httpEP = new IPEndPoint(localAddress, 80);
  83. Socket httpListener = null;
  84. try
  85. {
  86. httpListener = new Socket(httpEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
  87. httpListener.Bind(httpEP);
  88. httpListener.Listen(100);
  89. _httpListeners.Add(httpListener);
  90. _dnsServer.WriteLog("Web server was bound successfully: " + httpEP.ToString());
  91. }
  92. catch (Exception ex)
  93. {
  94. _dnsServer.WriteLog(ex);
  95. if (httpListener is not null)
  96. httpListener.Dispose();
  97. }
  98. }
  99. //bind to HTTPS port 443
  100. if (_webServerTlsCertificate is not null)
  101. {
  102. IPEndPoint httpsEP = new IPEndPoint(localAddress, 443);
  103. Socket httpsListener = null;
  104. try
  105. {
  106. httpsListener = new Socket(httpsEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
  107. httpsListener.Bind(httpsEP);
  108. httpsListener.Listen(100);
  109. _httpsListeners.Add(httpsListener);
  110. _dnsServer.WriteLog("Web server was bound successfully: " + httpsEP.ToString());
  111. }
  112. catch (Exception ex)
  113. {
  114. _dnsServer.WriteLog(ex);
  115. if (httpsListener is not null)
  116. httpsListener.Dispose();
  117. }
  118. }
  119. }
  120. //start reading requests
  121. int listenerTaskCount = Math.Max(1, Environment.ProcessorCount);
  122. foreach (Socket httpListener in _httpListeners)
  123. {
  124. for (int i = 0; i < listenerTaskCount; i++)
  125. {
  126. _ = Task.Factory.StartNew(delegate ()
  127. {
  128. return AcceptConnectionAsync(httpListener, false);
  129. }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Current);
  130. }
  131. }
  132. foreach (Socket httpsListener in _httpsListeners)
  133. {
  134. for (int i = 0; i < listenerTaskCount; i++)
  135. {
  136. _ = Task.Factory.StartNew(delegate ()
  137. {
  138. return AcceptConnectionAsync(httpsListener, true);
  139. }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Current);
  140. }
  141. }
  142. _state = ServiceState.Running;
  143. }
  144. private void StopWebServer()
  145. {
  146. if (_state != ServiceState.Running)
  147. return;
  148. _state = ServiceState.Stopping;
  149. foreach (Socket httpListener in _httpListeners)
  150. httpListener.Dispose();
  151. foreach (Socket httpsListener in _httpsListeners)
  152. httpsListener.Dispose();
  153. _httpListeners.Clear();
  154. _httpsListeners.Clear();
  155. _state = ServiceState.Stopped;
  156. }
  157. private void LoadWebServiceTlsCertificate(string webServerTlsCertificateFilePath, string webServerTlsCertificatePassword)
  158. {
  159. FileInfo fileInfo = new FileInfo(webServerTlsCertificateFilePath);
  160. if (!fileInfo.Exists)
  161. throw new ArgumentException("Web server TLS certificate file does not exists: " + webServerTlsCertificateFilePath);
  162. if (Path.GetExtension(webServerTlsCertificateFilePath) != ".pfx")
  163. throw new ArgumentException("Web server TLS certificate file must be PKCS #12 formatted with .pfx extension: " + webServerTlsCertificateFilePath);
  164. _webServerTlsCertificate = new X509Certificate2(webServerTlsCertificateFilePath, webServerTlsCertificatePassword);
  165. _webServerTlsCertificateLastModifiedOn = fileInfo.LastWriteTimeUtc;
  166. _dnsServer.WriteLog("Web server TLS certificate was loaded: " + webServerTlsCertificateFilePath);
  167. }
  168. private void StartTlsCertificateUpdateTimer()
  169. {
  170. if (_tlsCertificateUpdateTimer == null)
  171. {
  172. _tlsCertificateUpdateTimer = new Timer(delegate (object state)
  173. {
  174. if (!string.IsNullOrEmpty(_webServerTlsCertificateFilePath))
  175. {
  176. try
  177. {
  178. FileInfo fileInfo = new FileInfo(_webServerTlsCertificateFilePath);
  179. if (fileInfo.Exists && (fileInfo.LastWriteTimeUtc != _webServerTlsCertificateLastModifiedOn))
  180. LoadWebServiceTlsCertificate(_webServerTlsCertificateFilePath, _webServerTlsCertificatePassword);
  181. }
  182. catch (Exception ex)
  183. {
  184. _dnsServer.WriteLog("Web server encountered an error while updating TLS Certificate: " + _webServerTlsCertificateFilePath + "\r\n" + ex.ToString());
  185. }
  186. }
  187. }, null, TLS_CERTIFICATE_UPDATE_TIMER_INITIAL_INTERVAL, TLS_CERTIFICATE_UPDATE_TIMER_INTERVAL);
  188. }
  189. }
  190. private void StopTlsCertificateUpdateTimer()
  191. {
  192. if (_tlsCertificateUpdateTimer != null)
  193. {
  194. _tlsCertificateUpdateTimer.Dispose();
  195. _tlsCertificateUpdateTimer = null;
  196. }
  197. }
  198. private async Task AcceptConnectionAsync(Socket tcpListener, bool usingHttps)
  199. {
  200. try
  201. {
  202. tcpListener.SendTimeout = TCP_SEND_TIMEOUT;
  203. tcpListener.ReceiveTimeout = TCP_RECV_TIMEOUT;
  204. tcpListener.NoDelay = true;
  205. while (true)
  206. {
  207. Socket socket = await tcpListener.AcceptAsync();
  208. _ = ProcessConnectionAsync(socket, usingHttps);
  209. }
  210. }
  211. catch (SocketException ex)
  212. {
  213. if (ex.SocketErrorCode == SocketError.OperationAborted)
  214. return; //server stopping
  215. _dnsServer.WriteLog(ex);
  216. }
  217. catch (ObjectDisposedException)
  218. {
  219. //server stopped
  220. }
  221. catch (Exception ex)
  222. {
  223. if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped))
  224. return; //server stopping
  225. _dnsServer.WriteLog(ex);
  226. }
  227. }
  228. private async Task ProcessConnectionAsync(Socket socket, bool usingHttps)
  229. {
  230. try
  231. {
  232. IPEndPoint remoteEP = socket.RemoteEndPoint as IPEndPoint;
  233. Stream stream = new NetworkStream(socket);
  234. if (usingHttps)
  235. {
  236. SslStream httpsStream = new SslStream(stream);
  237. await httpsStream.AuthenticateAsServerAsync(_webServerTlsCertificate).WithTimeout(TCP_RECV_TIMEOUT);
  238. stream = httpsStream;
  239. }
  240. await ProcessHttpRequestAsync(stream, remoteEP, usingHttps);
  241. }
  242. catch (TimeoutException)
  243. {
  244. //ignore timeout exception on TLS auth
  245. }
  246. catch (IOException)
  247. {
  248. //ignore IO exceptions
  249. }
  250. catch (Exception ex)
  251. {
  252. _dnsServer.WriteLog(ex);
  253. }
  254. finally
  255. {
  256. socket.Dispose();
  257. }
  258. }
  259. private async Task ProcessHttpRequestAsync(Stream stream, IPEndPoint remoteEP, bool usingHttps)
  260. {
  261. try
  262. {
  263. while (true)
  264. {
  265. bool isSocketRemoteIpPrivate = NetUtilities.IsPrivateIP(remoteEP.Address);
  266. HttpRequest httpRequest = await HttpRequest.ReadRequestAsync(stream, 512).WithTimeout(TCP_RECV_TIMEOUT);
  267. if (httpRequest is null)
  268. return; //connection closed gracefully by client
  269. string requestConnection = httpRequest.Headers[HttpRequestHeader.Connection];
  270. if (string.IsNullOrEmpty(requestConnection))
  271. requestConnection = "close";
  272. string path = httpRequest.RequestPath;
  273. if (!path.StartsWith("/") || path.Contains("/../") || path.Contains("/.../"))
  274. {
  275. await SendErrorAsync(stream, requestConnection, 404);
  276. break;
  277. }
  278. if (path == "/")
  279. path = "/index.html";
  280. string accept = httpRequest.Headers[HttpRequestHeader.Accept];
  281. if (string.IsNullOrEmpty(accept) || accept.Contains("text/html", StringComparison.OrdinalIgnoreCase))
  282. {
  283. if (path.Equals("/index.html", StringComparison.OrdinalIgnoreCase))
  284. {
  285. //send block page
  286. if (_serveBlockPageFromWebServerRoot)
  287. {
  288. path = Path.GetFullPath(_webServerRootPath + path.Replace('/', Path.DirectorySeparatorChar));
  289. if (!path.StartsWith(_webServerRootPath) || !File.Exists(path))
  290. await SendErrorAsync(stream, requestConnection, 404);
  291. else
  292. await SendFileAsync(stream, requestConnection, path);
  293. }
  294. else
  295. {
  296. await SendContentAsync(stream, requestConnection, "text/html", _blockPageContent);
  297. }
  298. }
  299. else
  300. {
  301. //redirect to block page
  302. await RedirectAsync(stream, httpRequest.Protocol, requestConnection, (usingHttps ? "https://" : "http://") + httpRequest.Headers[HttpRequestHeader.Host]);
  303. }
  304. }
  305. else
  306. {
  307. if (_serveBlockPageFromWebServerRoot)
  308. {
  309. //serve files
  310. path = Path.GetFullPath(_webServerRootPath + path.Replace('/', Path.DirectorySeparatorChar));
  311. if (!path.StartsWith(_webServerRootPath) || !File.Exists(path))
  312. await SendErrorAsync(stream, requestConnection, 404);
  313. else
  314. await SendFileAsync(stream, requestConnection, path);
  315. }
  316. else
  317. {
  318. await SendErrorAsync(stream, requestConnection, 404);
  319. }
  320. }
  321. }
  322. }
  323. catch (TimeoutException)
  324. {
  325. //ignore timeout exception
  326. }
  327. catch (IOException)
  328. {
  329. //ignore IO exceptions
  330. }
  331. catch (Exception ex)
  332. {
  333. _dnsServer.WriteLog(ex);
  334. }
  335. }
  336. private static async Task SendContentAsync(Stream outputStream, string connection, string contentType, byte[] content)
  337. {
  338. byte[] bufferHeader = Encoding.UTF8.GetBytes("HTTP/1.1 200 OK\r\nDate: " + DateTime.UtcNow.ToString("r") + "\r\nContent-Type: " + contentType + "\r\nContent-Length: " + content.Length + "\r\nX-Robots-Tag: noindex, nofollow\r\nConnection: " + connection + "\r\n\r\n");
  339. await outputStream.WriteAsync(bufferHeader);
  340. await outputStream.WriteAsync(content);
  341. await outputStream.FlushAsync();
  342. }
  343. private static async Task SendErrorAsync(Stream outputStream, string connection, int statusCode, string message = null)
  344. {
  345. try
  346. {
  347. string statusString = statusCode + " " + GetHttpStatusString((HttpStatusCode)statusCode);
  348. byte[] bufferContent = Encoding.UTF8.GetBytes("<html><head><title>" + statusString + "</title></head><body><h1>" + statusString + "</h1>" + (message is null ? "" : "<p>" + message + "</p>") + "</body></html>");
  349. byte[] bufferHeader = Encoding.UTF8.GetBytes("HTTP/1.1 " + statusString + "\r\nDate: " + DateTime.UtcNow.ToString("r") + "\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: " + bufferContent.Length + "\r\nX-Robots-Tag: noindex, nofollow\r\nConnection: " + connection + "\r\n\r\n");
  350. await outputStream.WriteAsync(bufferHeader);
  351. await outputStream.WriteAsync(bufferContent);
  352. await outputStream.FlushAsync();
  353. }
  354. catch
  355. { }
  356. }
  357. private static async Task RedirectAsync(Stream outputStream, string protocol, string connection, string location)
  358. {
  359. try
  360. {
  361. string statusString = "302 Found";
  362. byte[] bufferContent = Encoding.UTF8.GetBytes("<html><head><title>" + statusString + "</title></head><body><h1>" + statusString + "</h1><p>Location: <a href=\"" + location + "\">" + location + "</a></p></body></html>");
  363. byte[] bufferHeader = Encoding.UTF8.GetBytes(protocol + " " + statusString + "\r\nDate: " + DateTime.UtcNow.ToString("r") + "\r\nLocation: " + location + "\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: " + bufferContent.Length + "\r\nX-Robots-Tag: noindex, nofollow\r\nConnection: " + connection + "\r\n\r\n");
  364. await outputStream.WriteAsync(bufferHeader);
  365. await outputStream.WriteAsync(bufferContent);
  366. await outputStream.FlushAsync();
  367. }
  368. catch
  369. { }
  370. }
  371. private static async Task SendFileAsync(Stream outputStream, string connection, string filePath)
  372. {
  373. using (FileStream fS = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
  374. {
  375. byte[] bufferHeader = Encoding.UTF8.GetBytes("HTTP/1.1 200 OK\r\nDate: " + DateTime.UtcNow.ToString("r") + "\r\nContent-Type: " + WebUtilities.GetContentType(filePath).MediaType + "\r\nContent-Length: " + fS.Length + "\r\nCache-Control: private, max-age=300\r\nX-Robots-Tag: noindex, nofollow\r\nConnection: " + connection + "\r\n\r\n");
  376. await outputStream.WriteAsync(bufferHeader);
  377. await fS.CopyToAsync(outputStream);
  378. await outputStream.FlushAsync();
  379. }
  380. }
  381. private static string GetHttpStatusString(HttpStatusCode statusCode)
  382. {
  383. StringBuilder sb = new StringBuilder();
  384. foreach (char c in statusCode.ToString().ToCharArray())
  385. {
  386. if (char.IsUpper(c) && sb.Length > 0)
  387. sb.Append(' ');
  388. sb.Append(c);
  389. }
  390. return sb.ToString();
  391. }
  392. #endregion
  393. #region public
  394. public async Task InitializeAsync(IDnsServer dnsServer, string config)
  395. {
  396. _dnsServer = dnsServer;
  397. using JsonDocument jsonDocument = JsonDocument.Parse(config);
  398. JsonElement jsonConfig = jsonDocument.RootElement;
  399. bool enableWebServer = jsonConfig.GetPropertyValue("enableWebServer", true);
  400. if (!enableWebServer)
  401. {
  402. StopWebServer();
  403. return;
  404. }
  405. _webServerLocalAddresses = jsonConfig.ReadArray("webServerLocalAddresses", IPAddress.Parse);
  406. if (jsonConfig.TryGetProperty("webServerUseSelfSignedTlsCertificate", out JsonElement jsonWebServerUseSelfSignedTlsCertificate))
  407. _webServerUseSelfSignedTlsCertificate = jsonWebServerUseSelfSignedTlsCertificate.GetBoolean();
  408. else
  409. _webServerUseSelfSignedTlsCertificate = true;
  410. _webServerTlsCertificateFilePath = jsonConfig.GetProperty("webServerTlsCertificateFilePath").GetString();
  411. _webServerTlsCertificatePassword = jsonConfig.GetProperty("webServerTlsCertificatePassword").GetString();
  412. _webServerRootPath = jsonConfig.GetProperty("webServerRootPath").GetString();
  413. if (!Path.IsPathRooted(_webServerRootPath))
  414. _webServerRootPath = Path.Combine(_dnsServer.ApplicationFolder, _webServerRootPath);
  415. _serveBlockPageFromWebServerRoot = jsonConfig.GetProperty("serveBlockPageFromWebServerRoot").GetBoolean();
  416. string blockPageTitle = jsonConfig.GetProperty("blockPageTitle").GetString();
  417. string blockPageHeading = jsonConfig.GetProperty("blockPageHeading").GetString();
  418. string blockPageMessage = jsonConfig.GetProperty("blockPageMessage").GetString();
  419. string blockPageContent = @"<html>
  420. <head>
  421. <title>" + (blockPageTitle is null ? "" : blockPageTitle) + @"</title>
  422. </head>
  423. <body>
  424. " + (blockPageHeading is null ? "" : " <h1>" + blockPageHeading + "</h1>") + @"
  425. " + (blockPageMessage is null ? "" : " <p>" + blockPageMessage + "</p>") + @"
  426. </body>
  427. </html>";
  428. _blockPageContent = Encoding.UTF8.GetBytes(blockPageContent);
  429. try
  430. {
  431. StopWebServer();
  432. string selfSignedCertificateFilePath = Path.Combine(_dnsServer.ApplicationFolder, "cert.pfx");
  433. if (_webServerUseSelfSignedTlsCertificate)
  434. {
  435. if (!File.Exists(selfSignedCertificateFilePath))
  436. {
  437. RSA rsa = RSA.Create(2048);
  438. CertificateRequest req = new CertificateRequest("cn=" + _dnsServer.ServerDomain, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
  439. X509Certificate2 cert = req.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(5));
  440. await File.WriteAllBytesAsync(selfSignedCertificateFilePath, cert.Export(X509ContentType.Pkcs12, null as string));
  441. }
  442. }
  443. else
  444. {
  445. File.Delete(selfSignedCertificateFilePath);
  446. }
  447. if (string.IsNullOrEmpty(_webServerTlsCertificateFilePath))
  448. {
  449. StopTlsCertificateUpdateTimer();
  450. if (_webServerUseSelfSignedTlsCertificate)
  451. {
  452. LoadWebServiceTlsCertificate(selfSignedCertificateFilePath, null);
  453. }
  454. else
  455. {
  456. //disable HTTPS
  457. _webServerTlsCertificate = null;
  458. }
  459. }
  460. else
  461. {
  462. LoadWebServiceTlsCertificate(_webServerTlsCertificateFilePath, _webServerTlsCertificatePassword);
  463. StartTlsCertificateUpdateTimer();
  464. }
  465. StartWebServer();
  466. }
  467. catch (Exception ex)
  468. {
  469. _dnsServer.WriteLog(ex);
  470. }
  471. if (!jsonConfig.TryGetProperty("webServerUseSelfSignedTlsCertificate", out _))
  472. {
  473. config = config.Replace("\"webServerTlsCertificateFilePath\"", "\"webServerUseSelfSignedTlsCertificate\": true,\r\n \"webServerTlsCertificateFilePath\"");
  474. await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config);
  475. }
  476. if (!jsonConfig.TryGetProperty("enableWebServer", out _))
  477. {
  478. config = config.Replace("\"webServerLocalAddresses\"", "\"enableWebServer\": true,\r\n \"webServerLocalAddresses\"");
  479. await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config);
  480. }
  481. }
  482. #endregion
  483. #region properties
  484. public string Description
  485. { 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) 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."; } }
  486. #endregion
  487. }
  488. }