123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871 |
- /*
- Technitium DNS Server
- Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com)
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
- using Microsoft.AspNetCore.Http;
- using System;
- using System.Collections.Concurrent;
- using System.Globalization;
- using System.IO;
- using System.IO.Compression;
- using System.Linq;
- using System.Net;
- using System.Text;
- using System.Threading;
- using System.Threading.Tasks;
- using TechnitiumLibrary.IO;
- using TechnitiumLibrary.Net.Dns;
- using TechnitiumLibrary.Net.Dns.EDnsOptions;
- using TechnitiumLibrary.Net.Dns.ResourceRecords;
- namespace DnsServerCore
- {
- public sealed class LogManager : IDisposable
- {
- #region variables
- static readonly char[] commaSeparator = new char[] { ',' };
- readonly string _configFolder;
- bool _enableLogging;
- string _logFolder;
- int _maxLogFileDays;
- bool _useLocalTime;
- const string LOG_ENTRY_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
- const string LOG_FILE_DATE_TIME_FORMAT = "yyyy-MM-dd";
- string _logFile;
- StreamWriter _logOut;
- DateTime _logDate;
- readonly BlockingCollection<LogQueueItem> _queue = new BlockingCollection<LogQueueItem>();
- Thread _consumerThread;
- readonly object _logFileLock = new object();
- readonly object _queueLock = new object();
- readonly AutoResetEvent _queueWait = new AutoResetEvent(false);
- CancellationTokenSource _queueCancellationTokenSource = new CancellationTokenSource();
- readonly Timer _logCleanupTimer;
- const int LOG_CLEANUP_TIMER_INITIAL_INTERVAL = 60 * 1000;
- const int LOG_CLEANUP_TIMER_PERIODIC_INTERVAL = 60 * 60 * 1000;
- readonly object _saveLock = new object();
- bool _pendingSave;
- readonly Timer _saveTimer;
- const int SAVE_TIMER_INITIAL_INTERVAL = 10000;
- #endregion
- #region constructor
- public LogManager(string configFolder)
- {
- _configFolder = configFolder;
- AppDomain.CurrentDomain.UnhandledException += delegate (object sender, UnhandledExceptionEventArgs e)
- {
- if (!_enableLogging)
- {
- Console.WriteLine(e.ExceptionObject.ToString());
- return;
- }
- lock (_queueLock)
- {
- try
- {
- _queueCancellationTokenSource.Cancel();
- lock (_logFileLock)
- {
- if (_logOut != null)
- WriteLog(DateTime.UtcNow, e.ExceptionObject.ToString());
- }
- }
- catch (ObjectDisposedException)
- { }
- catch (Exception ex)
- {
- Console.WriteLine(e.ExceptionObject.ToString());
- Console.WriteLine(ex.ToString());
- }
- finally
- {
- _queueWait.Set();
- }
- }
- };
- _logCleanupTimer = new Timer(delegate (object state)
- {
- try
- {
- if (_maxLogFileDays < 1)
- return;
- DateTime cutoffDate = DateTime.UtcNow.AddDays(_maxLogFileDays * -1).Date;
- DateTimeStyles dateTimeStyles;
- if (_useLocalTime)
- dateTimeStyles = DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal;
- else
- dateTimeStyles = DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal;
- foreach (string logFile in ListLogFiles())
- {
- string logFileName = Path.GetFileNameWithoutExtension(logFile);
- if (!DateTime.TryParseExact(logFileName, LOG_FILE_DATE_TIME_FORMAT, CultureInfo.InvariantCulture, dateTimeStyles, out DateTime logFileDate))
- continue;
- if (logFileDate < cutoffDate)
- {
- try
- {
- File.Delete(logFile);
- Write("LogManager cleanup deleted the log file: " + logFile);
- }
- catch (Exception ex)
- {
- Write(ex);
- }
- }
- }
- }
- catch (Exception ex)
- {
- Write(ex);
- }
- });
- LoadConfig();
- if (_enableLogging)
- StartLogging();
- _saveTimer = new Timer(delegate (object state)
- {
- lock (_saveLock)
- {
- if (_pendingSave)
- {
- try
- {
- SaveConfigInternal();
- _pendingSave = false;
- }
- catch (Exception ex)
- {
- Write(ex);
- //set timer to retry again
- _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);
- }
- }
- }
- });
- }
- #endregion
- #region IDisposable
- bool _disposed;
- private void Dispose(bool disposing)
- {
- lock (_saveLock)
- {
- _saveTimer?.Dispose();
- if (_pendingSave)
- {
- try
- {
- SaveConfigFileInternal();
- }
- catch (Exception ex)
- {
- Write(ex);
- }
- finally
- {
- _pendingSave = false;
- }
- }
- }
- lock (_queueLock)
- {
- try
- {
- _queueCancellationTokenSource.Cancel();
- lock (_logFileLock)
- {
- if (_disposed)
- return;
- if (disposing)
- {
- if (_logOut != null)
- {
- WriteLog(DateTime.UtcNow, "Logging stopped.");
- _logOut.Dispose();
- }
- _logCleanupTimer.Dispose();
- }
- _disposed = true;
- }
- }
- finally
- {
- _queueWait.Set();
- }
- }
- }
- public void Dispose()
- {
- Dispose(true);
- }
- #endregion
- #region private
- internal void StartLogging()
- {
- StartNewLog();
- _queueWait.Set();
- //start consumer thread
- _consumerThread = new Thread(delegate ()
- {
- while (true)
- {
- _queueWait.WaitOne();
- Monitor.Enter(_logFileLock);
- try
- {
- if (_disposed || (_logOut == null))
- break;
- foreach (LogQueueItem item in _queue.GetConsumingEnumerable(_queueCancellationTokenSource.Token))
- {
- if (_useLocalTime)
- {
- DateTime messageLocalDateTime = item._dateTime.ToLocalTime();
- if (messageLocalDateTime.Date > _logDate)
- {
- WriteLog(DateTime.UtcNow, "Logging stopped.");
- StartNewLog();
- }
- WriteLog(messageLocalDateTime, item._message);
- }
- else
- {
- if (item._dateTime.Date > _logDate)
- {
- WriteLog(DateTime.UtcNow, "Logging stopped.");
- StartNewLog();
- }
- WriteLog(item._dateTime, item._message);
- }
- }
- }
- catch (ObjectDisposedException)
- { }
- catch (OperationCanceledException)
- { }
- finally
- {
- Monitor.Exit(_logFileLock);
- }
- _queueCancellationTokenSource = new CancellationTokenSource();
- }
- });
- _consumerThread.Name = "Log";
- _consumerThread.IsBackground = true;
- _consumerThread.Start();
- }
- internal void StopLogging()
- {
- lock (_queueLock)
- {
- try
- {
- if (_logOut != null)
- _queueCancellationTokenSource.Cancel();
- lock (_logFileLock)
- {
- if (_logOut != null)
- {
- WriteLog(DateTime.UtcNow, "Logging stopped.");
- _logOut.Dispose();
- _logOut = null; //to stop consumer thread
- }
- }
- }
- finally
- {
- _queueWait.Set();
- }
- }
- }
- internal void LoadConfig()
- {
- string logConfigFile = Path.Combine(_configFolder, "log.config");
- try
- {
- using (FileStream fS = new FileStream(logConfigFile, FileMode.Open, FileAccess.Read))
- {
- BinaryReader bR = new BinaryReader(fS);
- if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "LS") //format
- throw new InvalidDataException("DnsServer log config file format is invalid.");
- byte version = bR.ReadByte();
- switch (version)
- {
- case 1:
- _enableLogging = bR.ReadBoolean();
- _logFolder = bR.ReadShortString();
- _maxLogFileDays = bR.ReadInt32();
- _useLocalTime = bR.ReadBoolean();
- break;
- default:
- throw new InvalidDataException("DnsServer log config version not supported.");
- }
- }
- }
- catch (FileNotFoundException)
- {
- _enableLogging = true;
- _logFolder = "logs";
- _maxLogFileDays = 365;
- _useLocalTime = false;
- SaveConfigFileInternal();
- }
- catch (Exception ex)
- {
- Console.Write(ex.ToString());
- SaveConfigFileInternal();
- }
- if (_maxLogFileDays == 0)
- _logCleanupTimer.Change(Timeout.Infinite, Timeout.Infinite);
- else
- _logCleanupTimer.Change(LOG_CLEANUP_TIMER_INITIAL_INTERVAL, LOG_CLEANUP_TIMER_PERIODIC_INTERVAL);
- }
- private string ConvertToRelativePath(string path)
- {
- if (path.StartsWith(_configFolder, Environment.OSVersion.Platform == PlatformID.Win32NT ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
- path = path.Substring(_configFolder.Length).TrimStart(Path.DirectorySeparatorChar);
- return path;
- }
- private string ConvertToAbsolutePath(string path)
- {
- if (Path.IsPathRooted(path))
- return path;
- return Path.Combine(_configFolder, path);
- }
- private void SaveConfigFileInternal()
- {
- string logConfigFile = Path.Combine(_configFolder, "log.config");
- using (MemoryStream mS = new MemoryStream())
- {
- //serialize config
- BinaryWriter bW = new BinaryWriter(mS);
- bW.Write(Encoding.ASCII.GetBytes("LS")); //format
- bW.Write((byte)1); //version
- bW.Write(_enableLogging);
- bW.WriteShortString(_logFolder);
- bW.Write(_maxLogFileDays);
- bW.Write(_useLocalTime);
- //write config
- mS.Position = 0;
- using (FileStream fS = new FileStream(logConfigFile, FileMode.Create, FileAccess.Write))
- {
- mS.CopyTo(fS);
- }
- }
- }
- private void SaveConfigInternal()
- {
- SaveConfigFileInternal();
- if (_logOut is null)
- {
- //stopped
- if (_enableLogging)
- StartLogging();
- }
- else
- {
- //running
- if (!_enableLogging)
- {
- StopLogging();
- }
- else if (!_logFile.StartsWith(ConvertToAbsolutePath(_logFolder)))
- {
- //log folder changed; restart logging to new folder
- StopLogging();
- StartLogging();
- }
- }
- }
- private void StartNewLog()
- {
- if (_logOut != null)
- _logOut.Dispose();
- string logFolder = ConvertToAbsolutePath(_logFolder);
- if (!Directory.Exists(logFolder))
- Directory.CreateDirectory(logFolder);
- DateTime logStartDateTime;
- if (_useLocalTime)
- logStartDateTime = DateTime.Now;
- else
- logStartDateTime = DateTime.UtcNow;
- _logFile = Path.Combine(logFolder, logStartDateTime.ToString(LOG_FILE_DATE_TIME_FORMAT) + ".log");
- _logOut = new StreamWriter(new FileStream(_logFile, FileMode.Append, FileAccess.Write, FileShare.Read));
- _logDate = logStartDateTime.Date;
- WriteLog(logStartDateTime, "Logging started.");
- }
- private void WriteLog(DateTime dateTime, string message)
- {
- if (_useLocalTime)
- {
- if (dateTime.Kind == DateTimeKind.Local)
- _logOut.WriteLine("[" + dateTime.ToString(LOG_ENTRY_DATE_TIME_FORMAT) + " Local] " + message);
- else
- _logOut.WriteLine("[" + dateTime.ToLocalTime().ToString(LOG_ENTRY_DATE_TIME_FORMAT) + " Local] " + message);
- }
- else
- {
- if (dateTime.Kind == DateTimeKind.Utc)
- _logOut.WriteLine("[" + dateTime.ToString(LOG_ENTRY_DATE_TIME_FORMAT) + " UTC] " + message);
- else
- _logOut.WriteLine("[" + dateTime.ToUniversalTime().ToString(LOG_ENTRY_DATE_TIME_FORMAT) + " UTC] " + message);
- }
- _logOut.Flush();
- }
- #endregion
- #region public
- public string[] ListLogFiles()
- {
- return Directory.GetFiles(ConvertToAbsolutePath(_logFolder), "*.log", SearchOption.TopDirectoryOnly);
- }
- public async Task DownloadLogAsync(HttpContext context, string logName, long limit)
- {
- string logFileName = logName + ".log";
- using (FileStream fS = new FileStream(Path.Combine(ConvertToAbsolutePath(_logFolder), logFileName), FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 64 * 1024, true))
- {
- HttpResponse response = context.Response;
- response.ContentType = "text/plain";
- response.Headers.ContentDisposition = "attachment;filename=" + logFileName;
- if ((limit > fS.Length) || (limit < 1))
- limit = fS.Length;
- OffsetStream oFS = new OffsetStream(fS, 0, limit);
- HttpRequest request = context.Request;
- Stream s;
- string acceptEncoding = request.Headers.AcceptEncoding;
- if (string.IsNullOrEmpty(acceptEncoding))
- {
- s = response.Body;
- }
- else
- {
- string[] acceptEncodingParts = acceptEncoding.Split(commaSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
- if (acceptEncodingParts.Contains("br"))
- {
- response.Headers.ContentEncoding = "br";
- s = new BrotliStream(response.Body, CompressionMode.Compress);
- }
- else if (acceptEncodingParts.Contains("gzip"))
- {
- response.Headers.ContentEncoding = "gzip";
- s = new GZipStream(response.Body, CompressionMode.Compress);
- }
- else if (acceptEncodingParts.Contains("deflate"))
- {
- response.Headers.ContentEncoding = "deflate";
- s = new DeflateStream(response.Body, CompressionMode.Compress);
- }
- else
- {
- s = response.Body;
- }
- }
- await using (s)
- {
- await oFS.CopyToAsync(s);
- if (fS.Length > limit)
- await s.WriteAsync(Encoding.UTF8.GetBytes("\r\n####___TRUNCATED___####"));
- }
- }
- }
- public void DeleteLog(string logName)
- {
- string logFile = Path.Combine(ConvertToAbsolutePath(_logFolder), logName + ".log");
- if (logFile.Equals(_logFile, StringComparison.OrdinalIgnoreCase))
- DeleteCurrentLogFile();
- else
- File.Delete(logFile);
- }
- public void DeleteAllLogs()
- {
- string[] logFiles = ListLogFiles();
- foreach (string logFile in logFiles)
- {
- if (logFile.Equals(_logFile, StringComparison.OrdinalIgnoreCase))
- DeleteCurrentLogFile();
- else
- File.Delete(logFile);
- }
- }
- public void Write(Exception ex)
- {
- Write(ex.ToString());
- }
- public void Write(IPEndPoint ep, Exception ex)
- {
- Write(ep, ex.ToString());
- }
- public void Write(IPEndPoint ep, string message)
- {
- string ipInfo;
- if (ep == null)
- ipInfo = "";
- else if (ep.Address.IsIPv4MappedToIPv6)
- ipInfo = "[" + ep.Address.MapToIPv4().ToString() + ":" + ep.Port + "] ";
- else
- ipInfo = "[" + ep.ToString() + "] ";
- Write(ipInfo + message);
- }
- public void Write(IPEndPoint ep, DnsTransportProtocol protocol, Exception ex)
- {
- Write(ep, protocol, ex.ToString());
- }
- public void Write(IPEndPoint ep, DnsTransportProtocol protocol, DnsDatagram request, DnsDatagram response)
- {
- DnsQuestionRecord q = null;
- if (request.Question.Count > 0)
- q = request.Question[0];
- string requestInfo;
- if (q is null)
- requestInfo = "MISSING QUESTION!";
- else
- requestInfo = "QNAME: " + q.Name + "; QTYPE: " + q.Type.ToString() + "; QCLASS: " + q.Class;
- if (request.Additional.Count > 0)
- {
- DnsResourceRecord lastRR = request.Additional[request.Additional.Count - 1];
- if ((lastRR.Type == DnsResourceRecordType.TSIG) && (lastRR.RDATA is DnsTSIGRecordData tsig))
- requestInfo += "; TSIG KeyName: " + lastRR.Name.ToLowerInvariant() + "; TSIG Algo: " + tsig.AlgorithmName + "; TSIG Error: " + tsig.Error.ToString();
- }
- string responseInfo;
- if (response is null)
- {
- responseInfo = "; NO RESPONSE FROM SERVER!";
- }
- else
- {
- responseInfo = "; RCODE: " + response.RCODE.ToString();
- string answer;
- if (response.Answer.Count == 0)
- {
- if (response.Truncation)
- answer = "[TRUNCATED]";
- else
- answer = "[]";
- }
- else if ((response.Answer.Count > 2) && response.IsZoneTransfer)
- {
- answer = "[ZONE TRANSFER]";
- }
- else
- {
- answer = "[";
- for (int i = 0; i < response.Answer.Count; i++)
- {
- if (i > 0)
- answer += ", ";
- answer += response.Answer[i].RDATA.ToString();
- }
- answer += "]";
- if (response.Additional.Count > 0)
- {
- switch (q.Type)
- {
- case DnsResourceRecordType.NS:
- case DnsResourceRecordType.MX:
- case DnsResourceRecordType.SRV:
- answer += "; ADDITIONAL: [";
- for (int i = 0; i < response.Additional.Count; i++)
- {
- DnsResourceRecord additional = response.Additional[i];
- switch (additional.Type)
- {
- case DnsResourceRecordType.A:
- case DnsResourceRecordType.AAAA:
- if (i > 0)
- answer += ", ";
- answer += additional.Name + " (" + additional.RDATA.ToString() + ")";
- break;
- }
- }
- answer += "]";
- break;
- }
- }
- }
- EDnsClientSubnetOptionData responseECS = response.GetEDnsClientSubnetOption();
- if (responseECS is not null)
- answer += "; ECS: " + responseECS.Address.ToString() + "/" + responseECS.ScopePrefixLength;
- responseInfo += "; ANSWER: " + answer;
- }
- Write(ep, protocol, requestInfo + responseInfo);
- }
- public void Write(IPEndPoint ep, DnsTransportProtocol protocol, string message)
- {
- Write(ep, protocol.ToString(), message);
- }
- public void Write(IPEndPoint ep, string protocol, string message)
- {
- string ipInfo;
- if (ep == null)
- ipInfo = "";
- else if (ep.Address.IsIPv4MappedToIPv6)
- ipInfo = "[" + ep.Address.MapToIPv4().ToString() + ":" + ep.Port + "] ";
- else
- ipInfo = "[" + ep.ToString() + "] ";
- Write(ipInfo + "[" + protocol.ToUpper() + "] " + message);
- }
- public void Write(string message)
- {
- if (_enableLogging)
- _queue.Add(new LogQueueItem(message));
- }
- public void DeleteCurrentLogFile()
- {
- lock (_queueLock)
- {
- try
- {
- if (_logOut != null)
- _queueCancellationTokenSource.Cancel();
- lock (_logFileLock)
- {
- if (_logOut != null)
- _logOut.Dispose();
- File.Delete(_logFile);
- if (_enableLogging)
- StartNewLog();
- }
- }
- finally
- {
- _queueWait.Set();
- }
- }
- }
- public void SaveConfig()
- {
- lock (_saveLock)
- {
- if (_pendingSave)
- return;
- _pendingSave = true;
- _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);
- }
- }
- #endregion
- #region properties
- public bool EnableLogging
- {
- get { return _enableLogging; }
- set { _enableLogging = value; }
- }
- public string LogFolder
- {
- get { return _logFolder; }
- set
- {
- string logFolder;
- if (string.IsNullOrEmpty(value))
- logFolder = "logs";
- else if (value.Length > 255)
- throw new ArgumentException("Log folder path length cannot exceed 255 characters.", nameof(LogFolder));
- else
- logFolder = value;
- Directory.CreateDirectory(ConvertToAbsolutePath(logFolder));
- _logFolder = ConvertToRelativePath(logFolder);
- }
- }
- public int MaxLogFileDays
- {
- get { return _maxLogFileDays; }
- set
- {
- if (value < 0)
- throw new ArgumentOutOfRangeException(nameof(MaxLogFileDays), "MaxLogFileDays must be greater than or equal to 0.");
- _maxLogFileDays = value;
- if (_maxLogFileDays == 0)
- _logCleanupTimer.Change(Timeout.Infinite, Timeout.Infinite);
- else
- _logCleanupTimer.Change(LOG_CLEANUP_TIMER_INITIAL_INTERVAL, LOG_CLEANUP_TIMER_PERIODIC_INTERVAL);
- }
- }
- public bool UseLocalTime
- {
- get { return _useLocalTime; }
- set { _useLocalTime = value; }
- }
- public string CurrentLogFile
- { get { return _logFile; } }
- public string LogFolderAbsolutePath
- { get { return ConvertToAbsolutePath(_logFolder); } }
- #endregion
- class LogQueueItem
- {
- #region variables
- public readonly DateTime _dateTime;
- public readonly string _message;
- #endregion
- #region constructor
- public LogQueueItem(string message)
- {
- _dateTime = DateTime.UtcNow;
- _message = message;
- }
- #endregion
- }
- }
- }
|