mirror of
https://github.com/fergalmoran/DnsServer.git
synced 2026-01-08 17:54:02 +00:00
AuthZoneManager: Implemented zone index to allow paginated access to zones list.
This commit is contained in:
@@ -45,7 +45,8 @@ namespace DnsServerCore.Dns.ZoneManagers
|
||||
|
||||
readonly AuthZoneTree _root = new AuthZoneTree();
|
||||
|
||||
int _totalZones;
|
||||
readonly List<AuthZoneInfo> _zoneIndex = new List<AuthZoneInfo>(10);
|
||||
readonly ReaderWriterLockSlim _zoneIndexLock = new ReaderWriterLockSlim();
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -94,7 +95,7 @@ namespace DnsServerCore.Dns.ZoneManagers
|
||||
//update authoritative zone SOA and NS records
|
||||
try
|
||||
{
|
||||
List<AuthZoneInfo> zones = GetAllZones();
|
||||
IReadOnlyList<AuthZoneInfo> zones = GetAllZones();
|
||||
|
||||
foreach (AuthZoneInfo zone in zones)
|
||||
{
|
||||
@@ -179,10 +180,7 @@ namespace DnsServerCore.Dns.ZoneManagers
|
||||
}
|
||||
|
||||
if (_root.TryAdd(zone))
|
||||
{
|
||||
_totalZones++;
|
||||
return zone;
|
||||
}
|
||||
|
||||
throw new DnsServerException("Zone already exists: " + zoneInfo.Name);
|
||||
}
|
||||
@@ -462,7 +460,16 @@ namespace DnsServerCore.Dns.ZoneManagers
|
||||
|
||||
internal void Flush()
|
||||
{
|
||||
_root.Clear();
|
||||
_zoneIndexLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_root.Clear();
|
||||
_zoneIndex.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_zoneIndexLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<DnsResourceRecord> CondenseIncrementalZoneTransferRecords(string zoneName, DnsResourceRecord currentSoaRecord, IReadOnlyList<DnsResourceRecord> xfrRecords)
|
||||
@@ -652,7 +659,7 @@ namespace DnsServerCore.Dns.ZoneManagers
|
||||
|
||||
public void LoadAllZoneFiles()
|
||||
{
|
||||
_root.Clear();
|
||||
Flush();
|
||||
|
||||
string zonesFolder = Path.Combine(_dnsServer.ConfigFolder, "zones");
|
||||
if (!Directory.Exists(zonesFolder))
|
||||
@@ -666,7 +673,7 @@ namespace DnsServerCore.Dns.ZoneManagers
|
||||
File.Move(oldZoneFile, Path.Combine(zonesFolder, Path.GetFileName(oldZoneFile)));
|
||||
}
|
||||
|
||||
//remove old internal zones
|
||||
//remove old internal zones files
|
||||
{
|
||||
string[] oldZoneFiles = new string[] { "localhost.zone", "1.0.0.127.in-addr.arpa.zone", "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa.zone" };
|
||||
|
||||
@@ -722,27 +729,38 @@ namespace DnsServerCore.Dns.ZoneManagers
|
||||
}
|
||||
|
||||
//load zone files
|
||||
string[] zoneFiles = Directory.GetFiles(zonesFolder, "*.zone");
|
||||
|
||||
foreach (string zoneFile in zoneFiles)
|
||||
_zoneIndexLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
using (FileStream fS = new FileStream(zoneFile, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
LoadZoneFrom(fS);
|
||||
}
|
||||
string[] zoneFiles = Directory.GetFiles(zonesFolder, "*.zone");
|
||||
|
||||
LogManager log = _dnsServer.LogManager;
|
||||
if (log != null)
|
||||
log.Write("DNS Server successfully loaded zone file: " + zoneFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
foreach (string zoneFile in zoneFiles)
|
||||
{
|
||||
LogManager log = _dnsServer.LogManager;
|
||||
if (log != null)
|
||||
log.Write("DNS Server failed to load zone file: " + zoneFile + "\r\n" + ex.ToString());
|
||||
try
|
||||
{
|
||||
using (FileStream fS = new FileStream(zoneFile, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
AuthZoneInfo zoneInfo = LoadZoneFrom(fS);
|
||||
_zoneIndex.Add(zoneInfo);
|
||||
}
|
||||
|
||||
LogManager log = _dnsServer.LogManager;
|
||||
if (log != null)
|
||||
log.Write("DNS Server successfully loaded zone file: " + zoneFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager log = _dnsServer.LogManager;
|
||||
if (log != null)
|
||||
log.Write("DNS Server failed to load zone file: " + zoneFile + "\r\n" + ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
_zoneIndex.Sort();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_zoneIndexLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -750,10 +768,21 @@ namespace DnsServerCore.Dns.ZoneManagers
|
||||
{
|
||||
PrimaryZone apexZone = new PrimaryZone(_dnsServer, zoneName, soaRecord, ns);
|
||||
|
||||
if (_root.TryAdd(apexZone))
|
||||
_zoneIndexLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_totalZones++;
|
||||
return new AuthZoneInfo(apexZone);
|
||||
if (_root.TryAdd(apexZone))
|
||||
{
|
||||
AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);
|
||||
_zoneIndex.Add(zoneInfo);
|
||||
_zoneIndex.Sort();
|
||||
|
||||
return zoneInfo;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_zoneIndexLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -763,10 +792,21 @@ namespace DnsServerCore.Dns.ZoneManagers
|
||||
{
|
||||
PrimaryZone apexZone = new PrimaryZone(_dnsServer, zoneName, primaryNameServer, @internal);
|
||||
|
||||
if (_root.TryAdd(apexZone))
|
||||
_zoneIndexLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_totalZones++;
|
||||
return new AuthZoneInfo(apexZone);
|
||||
if (_root.TryAdd(apexZone))
|
||||
{
|
||||
AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);
|
||||
_zoneIndex.Add(zoneInfo);
|
||||
_zoneIndex.Sort();
|
||||
|
||||
return zoneInfo;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_zoneIndexLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -776,11 +816,23 @@ namespace DnsServerCore.Dns.ZoneManagers
|
||||
{
|
||||
SecondaryZone apexZone = await SecondaryZone.CreateAsync(_dnsServer, zoneName, primaryNameServerAddresses, zoneTransferProtocol, tsigKeyName);
|
||||
|
||||
if (_root.TryAdd(apexZone))
|
||||
_zoneIndexLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
apexZone.TriggerRefresh(0);
|
||||
_totalZones++;
|
||||
return new AuthZoneInfo(apexZone);
|
||||
if (_root.TryAdd(apexZone))
|
||||
{
|
||||
apexZone.TriggerRefresh(0);
|
||||
|
||||
AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);
|
||||
_zoneIndex.Add(zoneInfo);
|
||||
_zoneIndex.Sort();
|
||||
|
||||
return zoneInfo;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_zoneIndexLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -790,11 +842,23 @@ namespace DnsServerCore.Dns.ZoneManagers
|
||||
{
|
||||
StubZone apexZone = await StubZone.CreateAsync(_dnsServer, zoneName, primaryNameServerAddresses);
|
||||
|
||||
if (_root.TryAdd(apexZone))
|
||||
_zoneIndexLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
apexZone.TriggerRefresh(0);
|
||||
_totalZones++;
|
||||
return new AuthZoneInfo(apexZone);
|
||||
if (_root.TryAdd(apexZone))
|
||||
{
|
||||
apexZone.TriggerRefresh(0);
|
||||
|
||||
AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);
|
||||
_zoneIndex.Add(zoneInfo);
|
||||
_zoneIndex.Sort();
|
||||
|
||||
return zoneInfo;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_zoneIndexLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -804,10 +868,21 @@ namespace DnsServerCore.Dns.ZoneManagers
|
||||
{
|
||||
ForwarderZone apexZone = new ForwarderZone(zoneName, forwarderProtocol, forwarder, dnssecValidation, proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword, fwdRecordComments);
|
||||
|
||||
if (_root.TryAdd(apexZone))
|
||||
_zoneIndexLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_totalZones++;
|
||||
return new AuthZoneInfo(apexZone);
|
||||
if (_root.TryAdd(apexZone))
|
||||
{
|
||||
AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);
|
||||
_zoneIndex.Add(zoneInfo);
|
||||
_zoneIndex.Sort();
|
||||
|
||||
return zoneInfo;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_zoneIndexLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -943,12 +1018,23 @@ namespace DnsServerCore.Dns.ZoneManagers
|
||||
|
||||
public bool DeleteZone(string zoneName)
|
||||
{
|
||||
if (_root.TryRemove(zoneName, out ApexZone apexZone))
|
||||
_zoneIndexLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
apexZone.Dispose();
|
||||
if (_root.TryRemove(zoneName, out ApexZone apexZone))
|
||||
{
|
||||
apexZone.Dispose();
|
||||
|
||||
_totalZones--;
|
||||
return true;
|
||||
AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);
|
||||
if (!_zoneIndex.Remove(zoneInfo))
|
||||
throw new InvalidOperationException("Zone deleted from tree but failed to remove from zone index.");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_zoneIndexLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -1698,31 +1784,49 @@ namespace DnsServerCore.Dns.ZoneManagers
|
||||
}
|
||||
}
|
||||
|
||||
public List<AuthZoneInfo> GetAllZones()
|
||||
public IReadOnlyList<AuthZoneInfo> GetAllZones()
|
||||
{
|
||||
List<AuthZoneInfo> zones = new List<AuthZoneInfo>();
|
||||
|
||||
foreach (AuthZoneNode zoneNode in _root)
|
||||
_zoneIndexLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
ApexZone apexZone = zoneNode.ApexZone;
|
||||
if (apexZone is null)
|
||||
continue;
|
||||
|
||||
AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);
|
||||
switch (zoneInfo.Type)
|
||||
{
|
||||
case AuthZoneType.Primary:
|
||||
case AuthZoneType.Secondary:
|
||||
case AuthZoneType.Stub:
|
||||
case AuthZoneType.Forwarder:
|
||||
zones.Add(zoneInfo);
|
||||
break;
|
||||
}
|
||||
return new List<AuthZoneInfo>(_zoneIndex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_zoneIndexLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
_totalZones = zones.Count;
|
||||
public ZonesPage GetZonesPage(int pageNumber, int zonesPerPage)
|
||||
{
|
||||
_zoneIndexLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
if (pageNumber < 0)
|
||||
pageNumber = int.MaxValue;
|
||||
else if (pageNumber == 0)
|
||||
pageNumber = 1;
|
||||
|
||||
return zones;
|
||||
int totalZones = _zoneIndex.Count;
|
||||
int totalPages = (totalZones / zonesPerPage) + (totalZones % zonesPerPage > 0 ? 1 : 0);
|
||||
|
||||
if (pageNumber > totalPages)
|
||||
pageNumber = totalPages;
|
||||
|
||||
int start = (pageNumber - 1) * zonesPerPage;
|
||||
int end = Math.Min(start + zonesPerPage, totalZones);
|
||||
|
||||
List<AuthZoneInfo> zones = new List<AuthZoneInfo>(end - start);
|
||||
|
||||
for (int i = start; i < end; i++)
|
||||
zones.Add(_zoneIndex[i]);
|
||||
|
||||
return new ZonesPage(pageNumber, totalPages, totalZones, zones);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_zoneIndexLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public void ListSubDomains(string domain, List<string> subDomains)
|
||||
@@ -2018,7 +2122,7 @@ namespace DnsServerCore.Dns.ZoneManagers
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadZoneFrom(Stream s)
|
||||
public AuthZoneInfo LoadZoneFrom(Stream s)
|
||||
{
|
||||
BinaryReader bR = new BinaryReader(s);
|
||||
|
||||
@@ -2030,108 +2134,110 @@ namespace DnsServerCore.Dns.ZoneManagers
|
||||
case 2:
|
||||
{
|
||||
DnsResourceRecord[] records = new DnsResourceRecord[bR.ReadInt32()];
|
||||
if (records.Length > 0)
|
||||
if (records.Length == 0)
|
||||
throw new InvalidDataException("Zone does not contain SOA record.");
|
||||
|
||||
DnsResourceRecord soaRecord = null;
|
||||
|
||||
for (int i = 0; i < records.Length; i++)
|
||||
{
|
||||
DnsResourceRecord soaRecord = null;
|
||||
records[i] = new DnsResourceRecord(s);
|
||||
|
||||
for (int i = 0; i < records.Length; i++)
|
||||
{
|
||||
records[i] = new DnsResourceRecord(s);
|
||||
|
||||
if (records[i].Type == DnsResourceRecordType.SOA)
|
||||
soaRecord = records[i];
|
||||
}
|
||||
|
||||
if (soaRecord == null)
|
||||
throw new InvalidDataException("Zone does not contain SOA record.");
|
||||
|
||||
//make zone info
|
||||
AuthZoneType zoneType;
|
||||
if (_dnsServer.ServerDomain.Equals((soaRecord.RDATA as DnsSOARecordData).PrimaryNameServer, StringComparison.OrdinalIgnoreCase))
|
||||
zoneType = AuthZoneType.Primary;
|
||||
else
|
||||
zoneType = AuthZoneType.Stub;
|
||||
|
||||
AuthZoneInfo zoneInfo = new AuthZoneInfo(records[0].Name, zoneType, false);
|
||||
|
||||
//create zone
|
||||
ApexZone apexZone = CreateEmptyZone(zoneInfo);
|
||||
|
||||
try
|
||||
{
|
||||
//load records
|
||||
LoadRecords(apexZone, records);
|
||||
}
|
||||
catch
|
||||
{
|
||||
DeleteZone(zoneInfo.Name);
|
||||
throw;
|
||||
}
|
||||
|
||||
//init zone
|
||||
switch (zoneInfo.Type)
|
||||
{
|
||||
case AuthZoneType.Primary:
|
||||
(apexZone as PrimaryZone).TriggerNotify();
|
||||
break;
|
||||
}
|
||||
if (records[i].Type == DnsResourceRecordType.SOA)
|
||||
soaRecord = records[i];
|
||||
}
|
||||
|
||||
if (soaRecord == null)
|
||||
throw new InvalidDataException("Zone does not contain SOA record.");
|
||||
|
||||
//make zone info
|
||||
AuthZoneType zoneType;
|
||||
if (_dnsServer.ServerDomain.Equals((soaRecord.RDATA as DnsSOARecordData).PrimaryNameServer, StringComparison.OrdinalIgnoreCase))
|
||||
zoneType = AuthZoneType.Primary;
|
||||
else
|
||||
zoneType = AuthZoneType.Stub;
|
||||
|
||||
AuthZoneInfo zoneInfo = new AuthZoneInfo(records[0].Name, zoneType, false);
|
||||
|
||||
//create zone
|
||||
ApexZone apexZone = CreateEmptyZone(zoneInfo);
|
||||
|
||||
try
|
||||
{
|
||||
//load records
|
||||
LoadRecords(apexZone, records);
|
||||
}
|
||||
catch
|
||||
{
|
||||
DeleteZone(zoneInfo.Name);
|
||||
throw;
|
||||
}
|
||||
|
||||
//init zone
|
||||
switch (zoneInfo.Type)
|
||||
{
|
||||
case AuthZoneType.Primary:
|
||||
(apexZone as PrimaryZone).TriggerNotify();
|
||||
break;
|
||||
}
|
||||
|
||||
return new AuthZoneInfo(apexZone);
|
||||
}
|
||||
break;
|
||||
|
||||
case 3:
|
||||
{
|
||||
bool zoneDisabled = bR.ReadBoolean();
|
||||
DnsResourceRecord[] records = new DnsResourceRecord[bR.ReadInt32()];
|
||||
if (records.Length > 0)
|
||||
if (records.Length == 0)
|
||||
throw new InvalidDataException("Zone does not contain SOA record.");
|
||||
|
||||
DnsResourceRecord soaRecord = null;
|
||||
|
||||
for (int i = 0; i < records.Length; i++)
|
||||
{
|
||||
DnsResourceRecord soaRecord = null;
|
||||
records[i] = new DnsResourceRecord(s);
|
||||
records[i].Tag = new AuthRecordInfo(bR, records[i].Type == DnsResourceRecordType.SOA);
|
||||
|
||||
for (int i = 0; i < records.Length; i++)
|
||||
{
|
||||
records[i] = new DnsResourceRecord(s);
|
||||
records[i].Tag = new AuthRecordInfo(bR, records[i].Type == DnsResourceRecordType.SOA);
|
||||
|
||||
if (records[i].Type == DnsResourceRecordType.SOA)
|
||||
soaRecord = records[i];
|
||||
}
|
||||
|
||||
if (soaRecord == null)
|
||||
throw new InvalidDataException("Zone does not contain SOA record.");
|
||||
|
||||
//make zone info
|
||||
AuthZoneType zoneType;
|
||||
if (_dnsServer.ServerDomain.Equals((soaRecord.RDATA as DnsSOARecordData).PrimaryNameServer, StringComparison.OrdinalIgnoreCase))
|
||||
zoneType = AuthZoneType.Primary;
|
||||
else
|
||||
zoneType = AuthZoneType.Stub;
|
||||
|
||||
AuthZoneInfo zoneInfo = new AuthZoneInfo(records[0].Name, zoneType, zoneDisabled);
|
||||
|
||||
//create zone
|
||||
ApexZone apexZone = CreateEmptyZone(zoneInfo);
|
||||
|
||||
try
|
||||
{
|
||||
//load records
|
||||
LoadRecords(apexZone, records);
|
||||
}
|
||||
catch
|
||||
{
|
||||
DeleteZone(zoneInfo.Name);
|
||||
throw;
|
||||
}
|
||||
|
||||
//init zone
|
||||
switch (zoneInfo.Type)
|
||||
{
|
||||
case AuthZoneType.Primary:
|
||||
(apexZone as PrimaryZone).TriggerNotify();
|
||||
break;
|
||||
}
|
||||
if (records[i].Type == DnsResourceRecordType.SOA)
|
||||
soaRecord = records[i];
|
||||
}
|
||||
|
||||
if (soaRecord == null)
|
||||
throw new InvalidDataException("Zone does not contain SOA record.");
|
||||
|
||||
//make zone info
|
||||
AuthZoneType zoneType;
|
||||
if (_dnsServer.ServerDomain.Equals((soaRecord.RDATA as DnsSOARecordData).PrimaryNameServer, StringComparison.OrdinalIgnoreCase))
|
||||
zoneType = AuthZoneType.Primary;
|
||||
else
|
||||
zoneType = AuthZoneType.Stub;
|
||||
|
||||
AuthZoneInfo zoneInfo = new AuthZoneInfo(records[0].Name, zoneType, zoneDisabled);
|
||||
|
||||
//create zone
|
||||
ApexZone apexZone = CreateEmptyZone(zoneInfo);
|
||||
|
||||
try
|
||||
{
|
||||
//load records
|
||||
LoadRecords(apexZone, records);
|
||||
}
|
||||
catch
|
||||
{
|
||||
DeleteZone(zoneInfo.Name);
|
||||
throw;
|
||||
}
|
||||
|
||||
//init zone
|
||||
switch (zoneInfo.Type)
|
||||
{
|
||||
case AuthZoneType.Primary:
|
||||
(apexZone as PrimaryZone).TriggerNotify();
|
||||
break;
|
||||
}
|
||||
|
||||
return new AuthZoneInfo(apexZone);
|
||||
}
|
||||
break;
|
||||
|
||||
case 4:
|
||||
{
|
||||
@@ -2181,8 +2287,9 @@ namespace DnsServerCore.Dns.ZoneManagers
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new AuthZoneInfo(apexZone);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidDataException("DNS Zone file version not supported.");
|
||||
@@ -2269,8 +2376,48 @@ namespace DnsServerCore.Dns.ZoneManagers
|
||||
}
|
||||
|
||||
public int TotalZones
|
||||
{ get { return _totalZones; } }
|
||||
{ get { return _zoneIndex.Count; } }
|
||||
|
||||
#endregion
|
||||
|
||||
public class ZonesPage
|
||||
{
|
||||
#region variables
|
||||
|
||||
readonly long _pageNumber;
|
||||
readonly long _totalPages;
|
||||
readonly long _totalZones;
|
||||
readonly IReadOnlyList<AuthZoneInfo> _zones;
|
||||
|
||||
#endregion
|
||||
|
||||
#region constructor
|
||||
|
||||
public ZonesPage(long pageNumber, long totalPages, long totalZones, IReadOnlyList<AuthZoneInfo> zones)
|
||||
{
|
||||
_pageNumber = pageNumber;
|
||||
_totalPages = totalPages;
|
||||
_totalZones = totalZones;
|
||||
_zones = zones;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region properties
|
||||
|
||||
public long PageNumber
|
||||
{ get { return _pageNumber; } }
|
||||
|
||||
public long TotalPages
|
||||
{ get { return _totalPages; } }
|
||||
|
||||
public long TotalZones
|
||||
{ get { return _totalZones; } }
|
||||
|
||||
public IReadOnlyList<AuthZoneInfo> Zones
|
||||
{ get { return _zones; } }
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user