1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
4 
5 
6 using System;
7 using System.Collections.Generic;
8 using System.Linq;
9 using System.Text;
10 using System.Threading;
11 using System.Runtime.CompilerServices;
12 using System.IO;
13 using System.Net;
14 using System.Diagnostics;
15 using System.Runtime.InteropServices;
16 
17 namespace Microsoft.Test.Data.SqlClient
18 {
19     /// <summary>
20     /// allows user to manipulate %windir%\system32\drivers\etc\hosts
21     /// the hosts file must be reverted if changed even if test application crashes, thus inherit from CriticalFinalizerObject. Make sure the instance is disposed after its use.
22     /// The last dispose call on the active instance reverts the hosts file.
23     ///
24     /// Usage:
25     /// using (var hostsFile = new HostsFileManager())
26     /// {
27     ///     // use the hostsFile methods to add/remove entries
28     ///     // simultaneous usage of HostsFileManager in two app domains or processes on the same machine is not allowed
29     /// }
30     /// </summary>
31     public sealed class HostsFileManager : IDisposable
32     {
33         // define global (machine-wide) lock instance
34         private static EventWaitHandle s_globalLock = new EventWaitHandle(true /* create as signalled */, EventResetMode.AutoReset, @"Global\HostsFileManagerLock");
35         private static bool s_globalLockTaken; // set when global (machine-wide) lock is in use
36 
37         private static int s_localUsageRefCount;
38         private static object s_localLock = new object();
39 
40         private static string s_hostsFilePath;
41         private static string s_backupPath;
42         private static bool s_hasBackup;
43         private static TextReader s_activeReader;
44         private static TextWriter s_activeWriter;
45         private static List<HostEntry> s_entriesCache;
46 
47         private const string HostsFilePathUnderSystem32 = @"C:\Windows\System32\drivers\etc\hosts";
48         private const string HostsFilePathUnderLinux = "/etc/hosts";
49         private const string HostsFilePathUnderMacOS = "/private/etc/hosts";
50 
51 
InitializeGlobal(ref bool mustRelease)52         private static void InitializeGlobal(ref bool mustRelease)
53         {
54             if (mustRelease)
55             {
56                 // already initialized
57                 return;
58             }
59 
60             lock (s_localLock)
61             {
62                 if (mustRelease)
63                 {
64                     // check again under lock
65                     return;
66                 }
67 
68                 if (s_localUsageRefCount > 0)
69                 {
70                     // initialized by another thread
71                     ++s_localUsageRefCount;
72                     return;
73                 }
74 
75                 // first call to initialize in this app domain
76                 // note: simultanious use of HostsFileManager is currently supported only within single AppDomain scope
77 
78                 // non-critical initialization goes first
79                 if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
80                 {
81                     s_hostsFilePath = HostsFilePathUnderSystem32;
82                 }
83                 else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
84                 {
85                     s_hostsFilePath = HostsFilePathUnderLinux;
86                 }
87                 else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
88                 {
89                     s_hostsFilePath = HostsFilePathUnderMacOS;
90                 }
91 
92                 s_backupPath = Path.Combine(Path.GetTempPath(), string.Format("Hosts_{0}.bak", Guid.NewGuid().ToString("N")));
93 
94                 // try to get global lock
95                 // note that once global lock is aquired, it must be released
96                 try { }
97                 finally
98                 {
99                     if (s_globalLock.WaitOne(0))
100                     {
101                         s_globalLockTaken = true;
102                         mustRelease = true;
103                         ++s_localUsageRefCount; // increment ref count for the first thread using the manager
104                     }
105                 }
106 
107                 if (!s_globalLockTaken)
108                 {
109                     throw new InvalidOperationException("HostsFileManager cannot initialize because hosts file is in use by another instance of the manager in the same or a different process (concurrent access is not allowed)");
110                 }
111 
112                 // locked now, take snapshot of hosts file and save it as a backup
113                 File.Copy(s_hostsFilePath, s_backupPath);
114                 s_hasBackup = true;
115 
116                 // load the current entries
117                 InternalRefresh();
118             }
119         }
120 
TerminateGlobal(ref bool originalMustRelease)121         private static void TerminateGlobal(ref bool originalMustRelease)
122         {
123             if (!originalMustRelease)
124             {
125                 // already disposed
126                 return;
127             }
128 
129             lock (s_localLock)
130             {
131                 if (!originalMustRelease)
132                 {
133                     // check again under lock
134                     return;
135                 }
136 
137                 // not yet disposed, do it now
138                 if (s_localUsageRefCount > 1)
139                 {
140                     // still in use by another thread(s)
141                     --s_localUsageRefCount;
142                     return;
143                 }
144 
145                 if (s_activeReader != null)
146                 {
147                     s_activeReader.Dispose();
148                     s_activeReader = null;
149                 }
150                 if (s_activeWriter != null)
151                 {
152                     s_activeWriter.Dispose();
153                     s_activeWriter = null;
154                 }
155                 bool deleteBackup = false;
156                 if (s_hasBackup)
157                 {
158                     // revert the hosts file
159                     File.Copy(s_backupPath, s_hostsFilePath, overwrite: true);
160                     s_hasBackup = false;
161                     deleteBackup = true;
162                 }
163 
164                 // Note: if critical finalizer fails to revert the hosts file, the global lock might remain reset until the machine is rebooted.
165                 // if this happens, Hosts file in unpredictable state so there is no point in running tests anyway
166                 if (s_globalLockTaken)
167                 {
168                     try { }
169                     finally
170                     {
171                         s_globalLock.Set();
172                         s_globalLockTaken = false;
173                         --s_localUsageRefCount; // decrement local ref count
174                         originalMustRelease = false;
175                     }
176                 }
177 
178                 // now we can destroy the backup
179                 if (deleteBackup)
180                 {
181                     File.Delete(s_backupPath);
182                 }
183             }
184         }
185 
186         private bool _mustRelease;
187         private bool _disposed;
188 
HostsFileManager()189         public HostsFileManager()
190         {
191             // lazy initialization
192             _mustRelease = false;
193             _disposed = false;
194         }
195 
~HostsFileManager()196         ~HostsFileManager()
197         {
198             Dispose(false);
199         }
200 
Dispose()201         public void Dispose()
202         {
203             Dispose(true);
204             GC.SuppressFinalize(this);
205         }
206 
Dispose(bool disposing)207         private void Dispose(bool disposing)
208         {
209             if (!_disposed)
210             {
211                 _disposed = true;
212                 TerminateGlobal(ref _mustRelease);
213             }
214         }
215 
216         public class HostEntry
217         {
HostEntry(string name, IPAddress address)218             public HostEntry(string name, IPAddress address)
219             {
220                 ValidateName(name);
221                 ValidateAddress(address);
222 
223                 this.Name = name;
224                 this.Address = address;
225             }
226 
227             public readonly string Name;
228             public readonly IPAddress Address;
229         }
230 
231         // helper methods
232 
233         // must be called under lock(_localLock) from each public API that uses static fields
InitializeLocal()234         private void InitializeLocal()
235         {
236             if (_disposed)
237             {
238                 throw new ObjectDisposedException(this.GetType().Name);
239             }
240 
241             InitializeGlobal(ref _mustRelease);
242         }
243 
244         private static readonly char[] s_whiteSpaceChars = new char[] { ' ', '\t' };
245 
ValidateName(string name)246         private static void ValidateName(string name)
247         {
248             if (string.IsNullOrEmpty(name) || name.IndexOfAny(s_whiteSpaceChars) >= 0)
249             {
250                 throw new ArgumentException("name cannot be null or empty or have whitespace characters in it");
251             }
252         }
253 
ValidateAddress(IPAddress address)254         private static void ValidateAddress(IPAddress address)
255         {
256             ValidateNonNull(address, "address");
257 
258             if (address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork &&
259                 address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetworkV6)
260             {
261                 throw new ArgumentException("only IPv4 or IPv6 addresses are allowed");
262             }
263         }
264 
265         private static void ValidateNonNull<T>(T value, string argName) where T : class
266         {
267             if (value == null)
268             {
269                 throw new ArgumentNullException(argName);
270             }
271         }
272 
TryParseLine(string line)273         private static HostEntry TryParseLine(string line)
274         {
275             line = line.Trim();
276             if (line.StartsWith("#"))
277             {
278                 // comment, ignore
279                 return null;
280             }
281 
282             string[] items = line.Split(s_whiteSpaceChars, StringSplitOptions.RemoveEmptyEntries);
283             if (items.Length == 0)
284             {
285                 // empty or white-space only line - ignore
286                 return null;
287             }
288 
289             if (items.Length != 2)
290             {
291                 Trace.WriteLine("Wrong entry in the hosts file (exactly two columns expected): \"" + line + "\"");
292                 return null;
293             }
294 
295             string name = items[1];
296             IPAddress address;
297             if (!IPAddress.TryParse(items[0], out address))
298             {
299                 Trace.WriteLine("Wrong entry in the hosts file (cannot parse the IP address): \"" + line + "\"");
300                 return null;
301             }
302 
303             try
304             {
305                 return new HostEntry(name, address);
306             }
307             catch (ArgumentException e)
308             {
309                 Console.WriteLine("Wrong entry in the hosts file, cannot create host entry: " + e.Message);
310                 return null;
311             }
312         }
313 
NameMatch(HostEntry entry, string name)314         private bool NameMatch(HostEntry entry, string name)
315         {
316             ValidateNonNull(entry, "entry");
317             ValidateName(name);
318 
319             return string.Equals(entry.Name, name, StringComparison.OrdinalIgnoreCase);
320         }
321 
322         // hosts file manipulation methods
323 
324         // reloads the hosts file, must be called under lock(_localLock)
InternalRefresh()325         private static void InternalRefresh()
326         {
327             List<HostEntry> entries = new List<HostEntry>();
328 
329             try
330             {
331                 s_activeReader = new StreamReader(new FileStream(s_hostsFilePath, FileMode.Open));
332 
333                 string line;
334                 while ((line = s_activeReader.ReadLine()) != null)
335                 {
336                     HostEntry nextEntry = TryParseLine(line);
337                     if (nextEntry != null)
338                     {
339                         entries.Add(nextEntry);
340                     }
341                 }
342             }
343             finally
344             {
345                 if (s_activeReader != null)
346                 {
347                     s_activeReader.Dispose();
348                     s_activeReader = null;
349                 }
350             }
351 
352             s_entriesCache = entries;
353         }
354 
355         // reloads the hosts file, must be called while still under lock(_localLock)
InternalSave()356         private void InternalSave()
357         {
358             try
359             {
360                 s_activeWriter = new StreamWriter(new FileStream(s_hostsFilePath, FileMode.Create));
361 
362                 foreach (HostEntry entry in s_entriesCache)
363                 {
364                     s_activeWriter.WriteLine("  {0}  {1}", entry.Address, entry.Name);
365                 }
366 
367                 s_activeWriter.Flush();
368             }
369             finally
370             {
371                 if (s_activeWriter != null)
372                 {
373                     s_activeWriter.Dispose();
374                     s_activeWriter = null;
375                 }
376             }
377         }
378 
RemoveAll(string name)379         public int RemoveAll(string name)
380         {
381             lock (s_localLock)
382             {
383                 InitializeLocal();
384                 ValidateName(name);
385 
386                 int removed = s_entriesCache.RemoveAll(entry => NameMatch(entry, name));
387 
388                 if (removed > 0)
389                 {
390                     InternalSave();
391                 }
392 
393                 return removed;
394             }
395         }
396 
EnumerateAddresses(string name)397         public IEnumerable<IPAddress> EnumerateAddresses(string name)
398         {
399             lock (s_localLock)
400             {
401                 InitializeLocal();
402                 ValidateName(name);
403 
404                 return from entry in s_entriesCache where NameMatch(entry, name) select entry.Address;
405             }
406         }
407 
Add(string name, IPAddress address)408         public void Add(string name, IPAddress address)
409         {
410             lock (s_localLock)
411             {
412                 InitializeLocal();
413 
414                 HostEntry entry = new HostEntry(name, address); // c-tor validates the arguments
415                 s_entriesCache.Add(entry);
416 
417                 InternalSave();
418             }
419         }
420 
Add(HostEntry entry)421         public void Add(HostEntry entry)
422         {
423             lock (s_localLock)
424             {
425                 InitializeLocal();
426                 ValidateNonNull(entry, "entry");
427 
428                 s_entriesCache.Add(entry);
429 
430                 InternalSave();
431             }
432         }
433 
AddRange(string name, IEnumerable<IPAddress> addresses)434         public void AddRange(string name, IEnumerable<IPAddress> addresses)
435         {
436             lock (s_localLock)
437             {
438                 InitializeLocal();
439                 ValidateName(name);
440                 ValidateNonNull(addresses, "addresses");
441 
442                 foreach (IPAddress address in addresses)
443                 {
444                     HostEntry entry = new HostEntry(name, address);
445 
446                     s_entriesCache.Add(entry);
447                 }
448 
449                 InternalSave();
450             }
451         }
452 
AddRange(IEnumerable<HostEntry> entries)453         public void AddRange(IEnumerable<HostEntry> entries)
454         {
455             lock (s_localLock)
456             {
457                 InitializeLocal();
458                 ValidateNonNull(entries, "entries");
459 
460                 foreach (HostEntry entry in entries)
461                 {
462                     ValidateNonNull(entry, "entries element");
463 
464                     s_entriesCache.Add(entry);
465                 }
466 
467                 InternalSave();
468             }
469         }
470 
Clear()471         public void Clear()
472         {
473             lock (s_localLock)
474             {
475                 InitializeLocal();
476 
477                 s_entriesCache.Clear();
478 
479                 InternalSave();
480             }
481         }
482     }
483 }
484