feature: Bootstrapper secure server discovery
This commit is contained in:
@@ -141,6 +141,7 @@
|
||||
<Compile Include="Extensions\EnrolExtensions.cs" />
|
||||
<Compile Include="Extensions\WhoAmIExtensions.cs" />
|
||||
<Compile Include="Interop\Certificates.cs" />
|
||||
<Compile Include="Interop\EndpointDiscovery.cs" />
|
||||
<Compile Include="Interop\Hardware.cs" />
|
||||
<Compile Include="Interop\LocalAuthentication.cs" />
|
||||
<Compile Include="Interop\Native\NetworkConnectionStatuses.cs" />
|
||||
|
||||
@@ -10,19 +10,18 @@ namespace Disco.Client
|
||||
{
|
||||
public static class ErrorReporting
|
||||
{
|
||||
private const string ServicePathTemplate = "http://DISCO:9292/Services/Client/ClientError";
|
||||
public static string DeviceIdentifier { get; set; }
|
||||
public static string EnrolmentSessionId { get; set; }
|
||||
|
||||
public static void ReportError(Exception Ex, bool ReportToServer)
|
||||
public static void ReportError(Exception exception, bool reportToServer)
|
||||
{
|
||||
bool isClientServiceException = Ex is ClientServiceException;
|
||||
bool isClientServiceException = exception is ClientServiceException;
|
||||
|
||||
ErrorReport report = new ErrorReport()
|
||||
{
|
||||
DeviceIdentifier = DeviceIdentifier,
|
||||
SessionId = EnrolmentSessionId,
|
||||
JsonException = Ex.IntenseExceptionSerialization()
|
||||
JsonException = exception.IntenseExceptionSerialization()
|
||||
};
|
||||
|
||||
try
|
||||
@@ -38,7 +37,7 @@ namespace Disco.Client
|
||||
catch (Exception) { }
|
||||
|
||||
// Don't log server errors back to the server
|
||||
if (!isClientServiceException && ReportToServer)
|
||||
if (!isClientServiceException && reportToServer)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -49,7 +48,7 @@ namespace Disco.Client
|
||||
|
||||
try
|
||||
{
|
||||
Presentation.WriteFatalError(Ex);
|
||||
Presentation.WriteFatalError(exception);
|
||||
}
|
||||
catch (Exception) { }
|
||||
}
|
||||
@@ -85,7 +84,9 @@ namespace Disco.Client
|
||||
string reportJson = JsonConvert.SerializeObject(report);
|
||||
string reportResponse;
|
||||
|
||||
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(ServicePathTemplate);
|
||||
var serverUri = new Uri(Program.ServerUrl ?? new Uri("http://disco:9292"), "/Services/Client/ClientError");
|
||||
|
||||
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(serverUri);
|
||||
request.UserAgent = $"Disco-Client/{Assembly.GetExecutingAssembly().GetName().Version.ToString(3)}";
|
||||
request.ContentType = "application/json";
|
||||
request.Method = WebRequestMethods.Http.Post;
|
||||
|
||||
@@ -1,30 +1,23 @@
|
||||
using Disco.Models.ClientServices;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Disco.Client.Extensions
|
||||
{
|
||||
public static class ClientServicesExtensions
|
||||
internal static class ClientServicesExtensions
|
||||
{
|
||||
//#if DEBUG
|
||||
// public const string ServicePathAuthenticatedTemplate = "http://WS-GSHARP:57252/Services/Client/Authenticated/{0}";
|
||||
// public const string ServicePathUnauthenticatedTemplate = "http://WS-GSHARP:57252/Services/Client/Unauthenticated/{0}";
|
||||
//#else
|
||||
public const string ServicePathAuthenticatedTemplate = "http://DISCO:9292/Services/Client/Authenticated/{0}";
|
||||
public const string ServicePathUnauthenticatedTemplate = "http://DISCO:9292/Services/Client/Unauthenticated/{0}";
|
||||
//#endif
|
||||
|
||||
public static ResponseType Post<ResponseType>(this ServiceBase<ResponseType> Service, bool Authenticated)
|
||||
public static ResponseType Post<ResponseType>(this ServiceBase<ResponseType> service, bool authenticated)
|
||||
{
|
||||
ResponseType serviceResponse;
|
||||
string serviceUrl;
|
||||
Uri serviceUrl;
|
||||
|
||||
if (Authenticated)
|
||||
serviceUrl = string.Format(ServicePathAuthenticatedTemplate, Service.Feature);
|
||||
if (authenticated)
|
||||
serviceUrl = new Uri(Program.ServerUrl, $"/Services/Client/Authenticated/{service.Feature}");
|
||||
else
|
||||
serviceUrl = string.Format(ServicePathUnauthenticatedTemplate, Service.Feature);
|
||||
serviceUrl = new Uri(Program.ServerUrl, $"/Services/Client/Unauthenticated/{service.Feature}");
|
||||
|
||||
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(serviceUrl);
|
||||
request.UserAgent = $"Disco-Client/{Assembly.GetExecutingAssembly().GetName().Version.ToString(3)}";
|
||||
@@ -39,7 +32,7 @@ namespace Disco.Client.Extensions
|
||||
{
|
||||
using (var jsonWriter = new JsonTextWriter(requestWriter))
|
||||
{
|
||||
jsonSerializer.Serialize(jsonWriter, Service);
|
||||
jsonSerializer.Serialize(jsonWriter, service);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace Disco.Client.Interop
|
||||
{
|
||||
internal class EndpointDiscovery
|
||||
{
|
||||
[DllImport("dnsapi", EntryPoint = "DnsQuery_W", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)]
|
||||
private static extern int DnsQuery([MarshalAs(UnmanagedType.VBByRefStr)] ref string pszName, NativeDnsQueryTypes wType, NativeDnsQueryOptions options, int aipServers, ref IntPtr ppQueryResults, int pReserved);
|
||||
|
||||
[DllImport("dnsapi", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
private static extern void DnsRecordListFree(IntPtr pRecordList, int FreeType);
|
||||
private const int DNS_ERROR_RCODE_NAME_ERROR = 0x232B;
|
||||
private const int DNS_ERROR_BAD_PACKET = 0x251E;
|
||||
public static Tuple<Uri, string> DiscoverServer(Uri forcedServerUri)
|
||||
{
|
||||
// 1. Check first command line argument for server name
|
||||
if (forcedServerUri != null)
|
||||
return Tuple.Create(forcedServerUri, "Manual");
|
||||
|
||||
// 2. Check for a DNS SRV record for _discoict._tcp.domain
|
||||
var domainSuffixes = new List<string>();
|
||||
var primaryDomain = IPGlobalProperties.GetIPGlobalProperties().DomainName;
|
||||
if (!string.IsNullOrEmpty(primaryDomain))
|
||||
domainSuffixes.Add(primaryDomain);
|
||||
var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(ni => ni.OperationalStatus == OperationalStatus.Up);
|
||||
foreach (var ni in networkInterfaces)
|
||||
{
|
||||
var domainSuffix = ni.GetIPProperties().DnsSuffix;
|
||||
if (!string.IsNullOrWhiteSpace(domainSuffix))
|
||||
{
|
||||
if (domainSuffix.Equals("mshome.net", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
if (!domainSuffixes.Contains(domainSuffix, StringComparer.OrdinalIgnoreCase))
|
||||
domainSuffixes.Add(domainSuffix);
|
||||
}
|
||||
}
|
||||
foreach (var domain in domainSuffixes)
|
||||
{
|
||||
var dnsRecords = GetSRVRecords("_discoict._tcp." + domain);
|
||||
if (dnsRecords.Count > 0)
|
||||
{
|
||||
var firstRecord = dnsRecords.OrderBy(r => r.Priority).ThenByDescending(r => r.Weight).First();
|
||||
if (firstRecord.Port == 443)
|
||||
return Tuple.Create(new Uri($"https://{firstRecord.Target}"), "SRV");
|
||||
else
|
||||
return Tuple.Create(new Uri($"https://{firstRecord.Target}:{firstRecord.Port}"), "SRV");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Detect VicSmart network and try resolving with Disco ICT Online Services
|
||||
if (TryResolveVicSmartServer(domainSuffixes, out var vicSmartServerUrl))
|
||||
return Tuple.Create(vicSmartServerUrl, "VicSmart");
|
||||
|
||||
// 4. Legacy: Ping 'disco' and assume port 9292
|
||||
using (Ping p = new Ping())
|
||||
{
|
||||
try
|
||||
{
|
||||
PingReply pr = p.Send("disco", 2000);
|
||||
if (pr.Status == IPStatus.Success)
|
||||
return Tuple.Create(new Uri("http://disco:9292"), "Legacy");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
throw new Exception("Could not locate Disco ICT server on the network.");
|
||||
}
|
||||
|
||||
private static bool TryResolveVicSmartServer(List<string> domainSuffixes, out Uri serverUrl)
|
||||
{
|
||||
if (IsVicSmartNetwork(domainSuffixes))
|
||||
{
|
||||
var potentialVicSmartAddresses = NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(ni => ni.OperationalStatus == OperationalStatus.Up)
|
||||
.SelectMany(ni => ni.GetIPProperties().UnicastAddresses)
|
||||
.Where(ua => ua.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
|
||||
.Select(ua => ua.Address.GetAddressBytes())
|
||||
.Where(a => a[0] == 10)
|
||||
.Select(a => (ushort)((a[1] >> 4) & 0x000F) | ((a[1] << 4) & 0x00F0) | ((a[2] << 12) & 0xF000) | ((a[2] << 4) & 0x0F00))
|
||||
.Distinct()
|
||||
.Select(a => $"{a:x4}.vicsmart.discoict.com")
|
||||
.ToList();
|
||||
|
||||
foreach (var potentialAddress in potentialVicSmartAddresses)
|
||||
{
|
||||
var records = GetTxtRecords(potentialAddress);
|
||||
|
||||
foreach (var record in records)
|
||||
{
|
||||
if (!record.Content.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
if (Uri.TryCreate(record.Content, UriKind.Absolute, out var discoveredUri))
|
||||
{
|
||||
serverUrl = discoveredUri;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serverUrl = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsVicSmartNetwork(List<string> domainSuffixes)
|
||||
{
|
||||
if (domainSuffixes.Any(s => string.Equals("services.education.vic.gov.au", s, StringComparison.OrdinalIgnoreCase)) ||
|
||||
domainSuffixes.Any(s => string.Equals("education.vic.gov.au", s, StringComparison.OrdinalIgnoreCase))
|
||||
)
|
||||
return true;
|
||||
|
||||
IPHostEntry doeWanDnsEntry;
|
||||
try
|
||||
{
|
||||
doeWanDnsEntry = Dns.GetHostEntry("broadband.doe.wan");
|
||||
if (doeWanDnsEntry.AddressList.Length > 0)
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{ }
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<DnsTxtRecord> GetTxtRecords(string name)
|
||||
{
|
||||
IntPtr resourceRecordsPointer = IntPtr.Zero;
|
||||
var records = new List<DnsTxtRecord>();
|
||||
var retry = 5;
|
||||
retry:
|
||||
try
|
||||
{
|
||||
int queryResult = DnsQuery(ref name, NativeDnsQueryTypes.DNS_TYPE_TEXT, NativeDnsQueryOptions.DNS_QUERY_STANDARD, 0, ref resourceRecordsPointer, 0);
|
||||
if (queryResult != 0)
|
||||
{
|
||||
if (queryResult == DNS_ERROR_RCODE_NAME_ERROR)
|
||||
return records;
|
||||
else if (queryResult == DNS_ERROR_BAD_PACKET && retry > 0)
|
||||
{
|
||||
// Sometimes a BAD_PACKET error is returned, retry a few times
|
||||
Thread.Sleep(200);
|
||||
retry--;
|
||||
goto retry;
|
||||
}
|
||||
else
|
||||
throw new Win32Exception(queryResult);
|
||||
}
|
||||
NativeDnsTxtRecord record;
|
||||
for (var resourceRecordPointer = resourceRecordsPointer; !resourceRecordPointer.Equals(IntPtr.Zero); resourceRecordPointer = record.pNext)
|
||||
{
|
||||
record = Marshal.PtrToStructure<NativeDnsTxtRecord>(resourceRecordPointer);
|
||||
if (record.wType == (ushort)NativeDnsQueryTypes.DNS_TYPE_TEXT)
|
||||
records.Add(DnsTxtRecord.FromNativeRecord(record));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (resourceRecordsPointer != IntPtr.Zero)
|
||||
DnsRecordListFree(resourceRecordsPointer, 0);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
private static List<DnsSrvRecord> GetSRVRecords(string name)
|
||||
{
|
||||
IntPtr resourceRecordsPointer = IntPtr.Zero;
|
||||
var records = new List<DnsSrvRecord>();
|
||||
var retry = 5;
|
||||
retry:
|
||||
try
|
||||
{
|
||||
int queryResult = DnsQuery(ref name, NativeDnsQueryTypes.DNS_TYPE_SRV, NativeDnsQueryOptions.DNS_QUERY_STANDARD, 0, ref resourceRecordsPointer, 0);
|
||||
if (queryResult != 0)
|
||||
{
|
||||
if (queryResult == DNS_ERROR_RCODE_NAME_ERROR)
|
||||
return records;
|
||||
else if (queryResult == DNS_ERROR_BAD_PACKET && retry > 0)
|
||||
{
|
||||
// Sometimes a BAD_PACKET error is returned, retry a few times
|
||||
Thread.Sleep(200);
|
||||
retry--;
|
||||
goto retry;
|
||||
}
|
||||
else
|
||||
throw new Win32Exception(queryResult);
|
||||
}
|
||||
NativeDnsSrvRecord record;
|
||||
for (var resourceRecordPointer = resourceRecordsPointer; !resourceRecordPointer.Equals(IntPtr.Zero); resourceRecordPointer = record.pNext)
|
||||
{
|
||||
record = Marshal.PtrToStructure<NativeDnsSrvRecord>(resourceRecordPointer);
|
||||
if (record.wType == (ushort)NativeDnsQueryTypes.DNS_TYPE_SRV)
|
||||
records.Add(DnsSrvRecord.FromNativeRecord(record));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (resourceRecordsPointer != IntPtr.Zero)
|
||||
DnsRecordListFree(resourceRecordsPointer, 0);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
private enum NativeDnsQueryOptions
|
||||
{
|
||||
DNS_QUERY_ACCEPT_TRUNCATED_RESPONSE = 1,
|
||||
DNS_QUERY_BYPASS_CACHE = 8,
|
||||
DNS_QUERY_DONT_RESET_TTL_VALUES = 0x100000,
|
||||
DNS_QUERY_NO_HOSTS_FILE = 0x40,
|
||||
DNS_QUERY_NO_LOCAL_NAME = 0x20,
|
||||
DNS_QUERY_NO_NETBT = 0x80,
|
||||
DNS_QUERY_NO_RECURSION = 4,
|
||||
DNS_QUERY_NO_WIRE_QUERY = 0x10,
|
||||
DNS_QUERY_RESERVED = -16777216,
|
||||
DNS_QUERY_RETURN_MESSAGE = 0x200,
|
||||
DNS_QUERY_STANDARD = 0,
|
||||
DNS_QUERY_TREAT_AS_FQDN = 0x1000,
|
||||
DNS_QUERY_USE_TCP_ONLY = 2,
|
||||
DNS_QUERY_WIRE_ONLY = 0x100
|
||||
}
|
||||
|
||||
private enum NativeDnsQueryTypes
|
||||
{
|
||||
DNS_TYPE_TEXT = 0x0010,
|
||||
DNS_TYPE_SRV = 0x0021
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct NativeDnsSrvRecord
|
||||
{
|
||||
public IntPtr pNext;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string pName;
|
||||
public ushort wType;
|
||||
public ushort wDataLength;
|
||||
public int flags;
|
||||
public int dwTtl;
|
||||
public int dwReserved;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string pNameTarget;
|
||||
public ushort wPriority;
|
||||
public ushort wWeight;
|
||||
public ushort wPort;
|
||||
public ushort Pad;
|
||||
}
|
||||
|
||||
private class DnsSrvRecord
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Type { get; set; }
|
||||
public int Ttl { get; set; }
|
||||
public string Target { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public int Weight { get; set; }
|
||||
public int Port { get; set; }
|
||||
|
||||
public static DnsSrvRecord FromNativeRecord(NativeDnsSrvRecord nativeRecord)
|
||||
{
|
||||
return new DnsSrvRecord
|
||||
{
|
||||
Name = nativeRecord.pName,
|
||||
Type = nativeRecord.wType,
|
||||
Ttl = nativeRecord.dwTtl,
|
||||
Target = nativeRecord.pNameTarget,
|
||||
Priority = nativeRecord.wPriority,
|
||||
Weight = nativeRecord.wWeight,
|
||||
Port = nativeRecord.wPort
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct NativeDnsTxtRecord
|
||||
{
|
||||
public IntPtr pNext;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string pName;
|
||||
public ushort wType;
|
||||
public ushort wDataLength;
|
||||
public int flags;
|
||||
public int dwTtl;
|
||||
public int dwReserved;
|
||||
public uint dwStringLength;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string pStringArray;
|
||||
}
|
||||
|
||||
private class DnsTxtRecord
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Type { get; set; }
|
||||
public int Ttl { get; set; }
|
||||
public string Content { get; set; }
|
||||
|
||||
public static DnsTxtRecord FromNativeRecord(NativeDnsTxtRecord nativeRecord)
|
||||
{
|
||||
return new DnsTxtRecord
|
||||
{
|
||||
Name = nativeRecord.pName,
|
||||
Type = nativeRecord.wType,
|
||||
Ttl = nativeRecord.dwTtl,
|
||||
Content = nativeRecord.pStringArray,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Disco.Client.Extensions;
|
||||
using Disco.Client.Interop;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
@@ -26,7 +27,7 @@ namespace Disco.Client
|
||||
}
|
||||
public static void UpdateStatus(string SubHeading, string Message, bool ShowProgress, int Progress)
|
||||
{
|
||||
Console.WriteLine($"#{SubHeading.EscapeMessage()},{Message.EscapeMessage()},{ShowProgress.ToString()},{Progress.ToString()}");
|
||||
Console.WriteLine($"#{SubHeading.EscapeMessage()},{Message.EscapeMessage()},{ShowProgress},{Progress}");
|
||||
}
|
||||
public static void TryDelay(int Milliseconds)
|
||||
{
|
||||
@@ -38,6 +39,11 @@ namespace Disco.Client
|
||||
{
|
||||
StringBuilder message = new StringBuilder();
|
||||
message.AppendLine($"Version: {Assembly.GetExecutingAssembly().GetName().Version.ToString(3)}");
|
||||
message.Append($"Server: {Program.ServerUrl})");
|
||||
if (Program.ServerUrl.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))
|
||||
message.AppendLine(" [Secure]");
|
||||
else
|
||||
message.AppendLine(" [Insecure]");
|
||||
message.AppendLine($"Device: {Hardware.Information.SerialNumber} ({Hardware.Information.Manufacturer} {Hardware.Information.Model})");
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
UpdateStatus("Preparation Client Started", message.ToString(), false, 0);
|
||||
@@ -48,12 +54,18 @@ namespace Disco.Client
|
||||
{
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
ClientServiceException clientServiceException = ex as ClientServiceException;
|
||||
if (clientServiceException != null)
|
||||
if (ex is ClientServiceException clientServiceException)
|
||||
{
|
||||
UpdateStatus($"An error occurred during {clientServiceException.ServiceFeature}",
|
||||
clientServiceException.Message, false, 0);
|
||||
}
|
||||
else if (ex is WebException exWeb &&
|
||||
exWeb.Response is HttpWebResponse webResponse &&
|
||||
webResponse.StatusCode == HttpStatusCode.InternalServerError)
|
||||
{
|
||||
UpdateStatus("Something went wrong on the server",
|
||||
"Review logs for more information (Configuration > Logging)", false, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
StringBuilder message = new StringBuilder();
|
||||
|
||||
+59
-6
@@ -1,6 +1,8 @@
|
||||
using Disco.Client.Extensions;
|
||||
using Disco.Client.Interop;
|
||||
using Disco.Models.ClientServices;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
|
||||
@@ -11,6 +13,9 @@ namespace Disco.Client
|
||||
public static bool IsAuthenticated { get; set; }
|
||||
public static bool RebootRequired { get; set; }
|
||||
public static bool AllowUninstall { get; set; }
|
||||
public static int BootstrapperVersion { get; private set; } = 1;
|
||||
public static int BootstrapperProcessId { get; private set; } = -1;
|
||||
public static Uri ServerUrl { get; private set; }
|
||||
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
@@ -24,12 +29,15 @@ namespace Disco.Client
|
||||
{
|
||||
Console.WriteLine("Waiting for Debugger to Attach");
|
||||
System.Threading.Thread.Sleep(1000);
|
||||
} while (!System.Diagnostics.Debugger.IsAttached);
|
||||
} while (!Debugger.IsAttached);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Initialize Environment Settings
|
||||
SetupEnvironment();
|
||||
SetupEnvironment(args);
|
||||
|
||||
if (ServerUrl == null)
|
||||
keepProcessing = DiscoverDiscoIct();
|
||||
|
||||
// Report to Bootstrapper
|
||||
Presentation.WriteBanner();
|
||||
@@ -45,7 +53,7 @@ namespace Disco.Client
|
||||
Presentation.WriteFooter(RebootRequired, AllowUninstall, !keepProcessing);
|
||||
}
|
||||
|
||||
public static void SetupEnvironment()
|
||||
public static void SetupEnvironment(string[] args)
|
||||
{
|
||||
// Hookup Unhandled Error Handling
|
||||
AppDomain.CurrentDomain.UnhandledException += ErrorReporting.CurrentDomain_UnhandledException;
|
||||
@@ -54,21 +62,66 @@ namespace Disco.Client
|
||||
WebRequest.DefaultWebProxy = new WebProxy();
|
||||
// Override Http 100 Continue Behaviour
|
||||
ServicePointManager.Expect100Continue = false;
|
||||
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
|
||||
|
||||
// Assume success unless otherwise notified
|
||||
AllowUninstall = true;
|
||||
|
||||
if (args != null && args.Length == 3)
|
||||
{
|
||||
// Parse Bootstrapper Version
|
||||
int parsedVersion;
|
||||
if (int.TryParse(args[0], out parsedVersion))
|
||||
BootstrapperVersion = parsedVersion;
|
||||
// Parse Bootstrapper Process ID
|
||||
int parsedProcessId;
|
||||
if (int.TryParse(args[1], out parsedProcessId))
|
||||
BootstrapperProcessId = parsedProcessId;
|
||||
// Parse Server URL
|
||||
Uri parsedUri;
|
||||
if (Uri.TryCreate(args[2], UriKind.Absolute, out parsedUri))
|
||||
ServerUrl = parsedUri;
|
||||
}
|
||||
else
|
||||
{
|
||||
BootstrapperVersion = 1;
|
||||
BootstrapperProcessId = -1;
|
||||
ServerUrl = null;
|
||||
}
|
||||
|
||||
// Detect Disco.Bootstrapper - Create Enable UI Delay if Running
|
||||
Presentation.DelayUI = false;
|
||||
try
|
||||
{
|
||||
Presentation.DelayUI = (System.Diagnostics.Process.GetProcessesByName("Disco.ClientBootstrapper").Length > 0);
|
||||
if (BootstrapperProcessId != -1)
|
||||
{
|
||||
var parentProcess = Process.GetProcessById(BootstrapperProcessId);
|
||||
Presentation.DelayUI = !parentProcess.HasExited;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Presentation.DelayUI = true; // Add Delays on Error
|
||||
}
|
||||
}
|
||||
|
||||
public static bool DiscoverDiscoIct()
|
||||
{
|
||||
try
|
||||
{
|
||||
Presentation.UpdateStatus("Detecting Disco ICT", "Locating Disco ICT Server, Please wait...", true, -1);
|
||||
Presentation.TryDelay(3000);
|
||||
ServerUrl = EndpointDiscovery.DiscoverServer(null).Item1;
|
||||
|
||||
// Complete
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorReporting.ReportError(ex, false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool WhoAmI()
|
||||
{
|
||||
try
|
||||
@@ -144,7 +197,7 @@ namespace Disco.Client
|
||||
var secondsConsumed = (DateTimeOffset.Now - startTime).TotalSeconds;
|
||||
var progress = (int)((secondsConsumed / totalSeconds) * 100);
|
||||
|
||||
Presentation.UpdateStatus($"Pending Device Enrolment Approval: {response.PendingIdentifier}", $"Waiting for enrolment session '{response.PendingIdentifier}' to be approved.{Environment.NewLine}Reason: {response.PendingReason}", true, progress);
|
||||
Presentation.UpdateStatus($"Pending Device Enrolment Approval: {response.PendingIdentifier}", $"Server: {Program.ServerUrl}{Environment.NewLine}Reason: {response.PendingReason}", true, progress);
|
||||
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(10));
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@ECHO OFF
|
||||
IF /I "%USERDOMAIN%"=="NT AUTHORITY" GOTO RunAsNetworkService
|
||||
Disco.Client.exe
|
||||
Disco.Client.exe %1 %2 %3
|
||||
EXIT /B 0
|
||||
|
||||
:RunAsNetworkService
|
||||
ECHO #Running,Launching Preparation Client, Please wait...{newline}Starting client as 'NT AUTHORITY\Network Service',true,-1
|
||||
PsExec -acceptula -i -u "NT AUTHORITY\Network Service" -w "%CD%" "%CD%\Start.bat"
|
||||
PsExec -acceptula -i -u "NT AUTHORITY\Network Service" -w "%CD%" "%CD%\Start.bat %1 %2 %3"
|
||||
EXIT /B 0
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using Disco.Client.Interop;
|
||||
using Disco.ClientBootstrapper.Interop;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
@@ -6,141 +8,115 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Disco.ClientBootstrapper
|
||||
{
|
||||
class BootstrapperLoop
|
||||
internal class BootstrapperLoop
|
||||
{
|
||||
|
||||
public Thread LoopThread;
|
||||
public delegate void LoopCompleteCallback();
|
||||
private LoopCompleteCallback mLoopCompleteCallback;
|
||||
private IStatus statusUI;
|
||||
private readonly Func<CancellationToken, Task> completeCallback;
|
||||
private readonly CancellationToken cancellationToken;
|
||||
private readonly IStatus statusUI;
|
||||
private readonly Uri forcedServerUrl;
|
||||
private string tempWorkingDirectory;
|
||||
private StringBuilder errorMessage;
|
||||
private Process clientProcess;
|
||||
|
||||
//#if DEBUG
|
||||
// public const string DiscoServerName = "WS-GSHARP";
|
||||
// public const int DiscoServerPort = 57252;
|
||||
//#else
|
||||
public const string DiscoServerName = "DISCO";
|
||||
public const int DiscoServerPort = 9292;
|
||||
//#endif
|
||||
|
||||
public BootstrapperLoop(IStatus StatusUI, LoopCompleteCallback Callback)
|
||||
public BootstrapperLoop(IStatus statusUI, Uri forcedServerUrl, Func<CancellationToken, Task> callback, CancellationToken cancellationToken)
|
||||
{
|
||||
statusUI = StatusUI;
|
||||
mLoopCompleteCallback = Callback;
|
||||
errorMessage = new StringBuilder();
|
||||
this.statusUI = statusUI;
|
||||
this.forcedServerUrl = forcedServerUrl;
|
||||
completeCallback = callback;
|
||||
this.cancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
LoopThread = new Thread(new ThreadStart(loopHost));
|
||||
LoopThread.Start();
|
||||
Task.Factory.StartNew(async () =>
|
||||
{
|
||||
await Loop(forcedServerUrl, cancellationToken);
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private void loopHost()
|
||||
private async Task Loop(Uri forcedServerUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
loop();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex.GetType() == typeof(ThreadAbortException))
|
||||
return;
|
||||
if (ex.GetType() == typeof(ThreadInterruptedException))
|
||||
return;
|
||||
Program.WriteAppError(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void loop()
|
||||
{
|
||||
|
||||
#if Debug
|
||||
statusUI.UpdateStatus("Waiting for Debugger", "Please wait...", true, -1);
|
||||
try
|
||||
{
|
||||
do
|
||||
{
|
||||
System.Threading.Thread.Sleep(10);
|
||||
} while (!System.Diagnostics.Debugger.IsAttached);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
statusUI.UpdateStatus("Error", ex.Message, true, -1);
|
||||
return;
|
||||
}
|
||||
#else
|
||||
statusUI.UpdateStatus("System Preparation (Bootstrapper)", "Starting", "Please wait...", true, -1);
|
||||
#endif
|
||||
|
||||
tempWorkingDirectory = Path.Combine(Path.GetPathRoot(Environment.SystemDirectory), "Disco\\Temp");
|
||||
tempWorkingDirectory = Path.Combine(Path.GetPathRoot(Environment.SystemDirectory), @"Disco\Temp");
|
||||
if (!Directory.Exists(tempWorkingDirectory))
|
||||
Directory.CreateDirectory(tempWorkingDirectory);
|
||||
|
||||
// Check for Network Connectivity
|
||||
statusUI.UpdateStatus(null, "Detecting Network", "Checking network connectivity, Please wait...", true, -1);
|
||||
if (!Interop.NetworkInterop.PingDiscoIct(DiscoServerName))
|
||||
if (!NetworkInterop.HasNetworkConnectivity())
|
||||
{
|
||||
statusUI.UpdateStatus(null, "Detecting Network", "No network connectivity detected, Diagnosing...", true, -1);
|
||||
statusUI_WriteAdapterInfo();
|
||||
|
||||
if (!Interop.NetworkInterop.PingDiscoIct(DiscoServerName))
|
||||
if (!NetworkInterop.HasNetworkConnectivity())
|
||||
{
|
||||
// Check for Wireless
|
||||
var hasWireless = (Interop.NetworkInterop.NetworkAdapters.Count(na => na.IsWireless) > 0);
|
||||
var hasWireless = (NetworkInterop.NetworkAdapters.Count(na => na.IsWireless) > 0);
|
||||
if (hasWireless)
|
||||
{
|
||||
// True: Do wireless loop
|
||||
statusUI.UpdateStatus(null, "Configuring Wireless Network", "Wireless adapter detected, Configuring...", true, -1);
|
||||
Interop.NetworkInterop.ConfigureWireless();
|
||||
await NetworkInterop.ConfigureWireless(cancellationToken);
|
||||
statusUI.UpdateStatus(null, "Waiting for Wireless Network", null, true, 0);
|
||||
for (int i = 0; i < 100; i++)
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
statusUI_WriteAdapterInfo();
|
||||
statusUI.UpdateStatus(null, null, null, true, i);
|
||||
Program.SleepThread(500, false);
|
||||
if (Interop.NetworkInterop.PingDiscoIct(DiscoServerName))
|
||||
await Program.SleepThread(2000, false, cancellationToken);
|
||||
if (NetworkInterop.HasNetworkConnectivity())
|
||||
break;
|
||||
}
|
||||
if (!Interop.NetworkInterop.PingDiscoIct(DiscoServerName))
|
||||
if (!NetworkInterop.HasNetworkConnectivity())
|
||||
{
|
||||
statusUI.UpdateStatus(null, "Wireless Network Failed", "Unable to connect to the wireless network, please connect the network cable...", false);
|
||||
Program.SleepThread(3000, false);
|
||||
await Program.SleepThread(3000, false, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Interop.NetworkInterop.PingDiscoIct(DiscoServerName))
|
||||
if (!NetworkInterop.HasNetworkConnectivity())
|
||||
{
|
||||
// Instruct user to connect network cable
|
||||
statusUI.UpdateStatus(null, "Please connect the network cable", null);
|
||||
for (int i = 0; i < 100; i++)
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
statusUI_WriteAdapterInfo();
|
||||
statusUI.UpdateStatus(null, null, null, true, i);
|
||||
Program.SleepThread(500, false);
|
||||
if (Interop.NetworkInterop.PingDiscoIct(DiscoServerName))
|
||||
await Program.SleepThread(2000, false, cancellationToken);
|
||||
if (NetworkInterop.HasNetworkConnectivity())
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Interop.NetworkInterop.PingDiscoIct(DiscoServerName))
|
||||
if (!NetworkInterop.HasNetworkConnectivity())
|
||||
{
|
||||
// Client Failed
|
||||
if (mLoopCompleteCallback != null)
|
||||
{
|
||||
mLoopCompleteCallback.BeginInvoke(null, null);
|
||||
}
|
||||
if (completeCallback != null)
|
||||
await completeCallback(cancellationToken);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Tuple<Uri, string> serverDiscovery;
|
||||
statusUI.UpdateStatus(null, "Detecting Disco ICT", "Locating Disco ICT Server, Please wait...", true, -1);
|
||||
try
|
||||
{
|
||||
serverDiscovery = EndpointDiscovery.DiscoverServer(forcedServerUrl);
|
||||
statusUI.UpdateStatus(null, null, $"{serverDiscovery.Item1} ({serverDiscovery.Item2})", true, -1);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
statusUI.UpdateStatus(null, null, "Failed to locate Disco ICT Server, exiting...", true, -1);
|
||||
await Program.SleepThread(2000, false, cancellationToken);
|
||||
throw;
|
||||
}
|
||||
|
||||
// Download Client
|
||||
statusUI.UpdateStatus(null, "Downloading", "Retrieving Preparation Client, Please wait...", true, -1);
|
||||
string clientSourceLocation = Path.Combine(tempWorkingDirectory, "PreparationClient.zip");
|
||||
@@ -148,8 +124,29 @@ namespace Disco.ClientBootstrapper
|
||||
{
|
||||
// Don't use a proxy when downloading the Client
|
||||
webClient.Proxy = new WebProxy();
|
||||
|
||||
webClient.DownloadFile($"http://{DiscoServerName}:{DiscoServerPort}/Services/Client/PreparationClient", clientSourceLocation);
|
||||
webClient.Headers.Add("X-DiscoICT-Discovery", serverDiscovery.Item2);
|
||||
try
|
||||
{
|
||||
webClient.DownloadFile(new Uri(serverDiscovery.Item1, "/Services/Client/PreparationClient"), clientSourceLocation);
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
if (ex.Response != null &&
|
||||
ex.Response is HttpWebResponse response)
|
||||
{
|
||||
if (response.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
statusUI.UpdateStatus(null, "Download failed: Bad Request", response.StatusDescription, true, -1);
|
||||
await Program.SleepThread(5000, false, cancellationToken);
|
||||
}
|
||||
else if (response.StatusCode == HttpStatusCode.InternalServerError)
|
||||
{
|
||||
statusUI.UpdateStatus(null, "Download failed: Something went wrong on the server", "Review logs for more information (Configuration > Logging)", true, -1);
|
||||
await Program.SleepThread(5000, false, cancellationToken);
|
||||
}
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Unzip Client
|
||||
@@ -166,7 +163,7 @@ namespace Disco.ClientBootstrapper
|
||||
|
||||
// Launch Client
|
||||
statusUI.UpdateStatus("System Preparation (Client)", "Running", "Launching Preparation Client, Please wait...", true, -1);
|
||||
ProcessStartInfo clientProcessStart = new ProcessStartInfo(Path.Combine(clientLocation, "Start.bat"))
|
||||
ProcessStartInfo clientProcessStart = new ProcessStartInfo(Path.Combine(clientLocation, "Start.bat"), $"2 {Process.GetCurrentProcess().Id} {serverDiscovery.Item1}")
|
||||
{
|
||||
WorkingDirectory = clientLocation,
|
||||
CreateNoWindow = true,
|
||||
@@ -194,26 +191,29 @@ namespace Disco.ClientBootstrapper
|
||||
// Cleanup
|
||||
if (Directory.Exists(tempWorkingDirectory))
|
||||
Directory.Delete(tempWorkingDirectory, true);
|
||||
Interop.CertificateInterop.RemoveTempCerts();
|
||||
CertificateInterop.RemoveTempCerts();
|
||||
|
||||
// Pause if Error
|
||||
if (errorMessage.Length > 0)
|
||||
{
|
||||
Program.SleepThread(10000, true);
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex.GetType() == typeof(ThreadAbortException))
|
||||
return;
|
||||
if (ex.GetType() == typeof(ThreadInterruptedException))
|
||||
return;
|
||||
if (ex.GetType() == typeof(OperationCanceledException))
|
||||
return;
|
||||
Program.WriteAppError(ex);
|
||||
}
|
||||
// End Of Loop
|
||||
if (mLoopCompleteCallback != null)
|
||||
{
|
||||
mLoopCompleteCallback.BeginInvoke(null, null);
|
||||
}
|
||||
if (completeCallback != null)
|
||||
await completeCallback(cancellationToken);
|
||||
}
|
||||
|
||||
void statusUI_WriteAdapterInfo()
|
||||
private void statusUI_WriteAdapterInfo()
|
||||
{
|
||||
|
||||
var info = new StringBuilder();
|
||||
foreach (var na in Interop.NetworkInterop.NetworkAdapters)
|
||||
foreach (var na in NetworkInterop.NetworkAdapters)
|
||||
{
|
||||
if (na.IsWireless)
|
||||
{
|
||||
@@ -228,11 +228,10 @@ namespace Disco.ClientBootstrapper
|
||||
|
||||
}
|
||||
|
||||
void clientProcess_OutputDataReceived(object sender, DataReceivedEventArgs e)
|
||||
private void clientProcess_OutputDataReceived(object sender, DataReceivedEventArgs e)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(e.Data))
|
||||
{
|
||||
Debug.WriteLine($"OUTPUT: {e.Data}");
|
||||
var data = e.Data.Substring(1).Split(new char[] { ',' });
|
||||
switch (e.Data[0])
|
||||
{
|
||||
@@ -249,15 +248,5 @@ namespace Disco.ClientBootstrapper
|
||||
}
|
||||
}
|
||||
|
||||
//void clientProcess_ErrorDataReceived(object sender, DataReceivedEventArgs e)
|
||||
//{
|
||||
// if (!string.IsNullOrEmpty(e.Data))
|
||||
// {
|
||||
// System.Diagnostics.Debug.WriteLine(string.Format("ERROR: {0}", e.Data));
|
||||
// this.errorMessage.AppendLine(e.Data);
|
||||
// statusUI.UpdateStatus(null, "An Error Occurred", this.errorMessage.ToString(), false);
|
||||
// }
|
||||
//}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,9 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Disco.Client\Interop\EndpointDiscovery.cs">
|
||||
<Link>Interop\EndpointDiscovery.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Resources\Libraries\DotNetZip\Source\BZip2\BitWriter.cs">
|
||||
<Link>DotNetZip\BZip2\BitWriter.cs</Link>
|
||||
</Compile>
|
||||
|
||||
@@ -7,22 +7,22 @@ namespace Disco.ClientBootstrapper
|
||||
{
|
||||
|
||||
private delegate void dUpdateStatus(string Heading, string SubHeading, string Message, bool? ShowProgress, int? Progress);
|
||||
private dUpdateStatus mUpdateStatus;
|
||||
private readonly dUpdateStatus mUpdateStatus;
|
||||
|
||||
public FormStatus()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
|
||||
this.labelVersion.Text = $"v{version.ToString(3)}";
|
||||
labelVersion.Text = $"v{version.ToString(3)}";
|
||||
|
||||
this.FormClosed += new FormClosedEventHandler(FormStatus_FormClosed);
|
||||
FormClosed += new FormClosedEventHandler(FormStatus_FormClosed);
|
||||
|
||||
mUpdateStatus = new dUpdateStatus(UpdateStatusDo);
|
||||
Cursor.Hide();
|
||||
}
|
||||
|
||||
void FormStatus_FormClosed(object sender, FormClosedEventArgs e)
|
||||
private void FormStatus_FormClosed(object sender, FormClosedEventArgs e)
|
||||
{
|
||||
Cursor.Show();
|
||||
Program.ExitApplication();
|
||||
@@ -32,43 +32,43 @@ namespace Disco.ClientBootstrapper
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Invoke(mUpdateStatus, Heading, SubHeading, Message, ShowProgress, Progress);
|
||||
Invoke(mUpdateStatus, Heading, SubHeading, Message, ShowProgress, Progress);
|
||||
}
|
||||
catch (Exception) { }
|
||||
}
|
||||
private void UpdateStatusDo(string Heading, string SubHeading, string Message, bool? ShowProgress, int? Progress)
|
||||
{
|
||||
if (Heading != null)
|
||||
if (this.labelHeading.Text != Heading)
|
||||
this.labelHeading.Text = Heading;
|
||||
if (labelHeading.Text != Heading)
|
||||
labelHeading.Text = Heading;
|
||||
if (SubHeading != null)
|
||||
if (this.labelSubHeading.Text != SubHeading)
|
||||
this.labelSubHeading.Text = SubHeading;
|
||||
if (labelSubHeading.Text != SubHeading)
|
||||
labelSubHeading.Text = SubHeading;
|
||||
if (Message != null)
|
||||
if (this.labelMessage.Text != Message)
|
||||
this.labelMessage.Text = Message;
|
||||
if (labelMessage.Text != Message)
|
||||
labelMessage.Text = Message;
|
||||
|
||||
if (ShowProgress.HasValue)
|
||||
{
|
||||
if (ShowProgress.Value)
|
||||
{
|
||||
this.progressBar.Visible = true;
|
||||
progressBar.Visible = true;
|
||||
if (Progress.HasValue)
|
||||
{
|
||||
if (Progress.Value >= 0)
|
||||
{
|
||||
this.progressBar.Value = Math.Min(Progress.Value, 100);
|
||||
this.progressBar.Style = ProgressBarStyle.Continuous;
|
||||
progressBar.Value = Math.Min(Progress.Value, 100);
|
||||
progressBar.Style = ProgressBarStyle.Continuous;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.progressBar.Style = ProgressBarStyle.Marquee;
|
||||
progressBar.Style = ProgressBarStyle.Marquee;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.progressBar.Visible = false;
|
||||
progressBar.Visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,36 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Disco.ClientBootstrapper
|
||||
{
|
||||
class InstallLoop
|
||||
internal class InstallLoop
|
||||
{
|
||||
private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
|
||||
private readonly string installLocation;
|
||||
private readonly string wimImageId;
|
||||
private readonly string tempPath;
|
||||
private readonly Action completeCallback;
|
||||
private readonly Uri forcedServerUrl;
|
||||
|
||||
public Thread LoopThread;
|
||||
public delegate void CompleteCallback();
|
||||
private CompleteCallback mCompleteCallback;
|
||||
private string InstallLocation;
|
||||
private string WimImageId;
|
||||
private string TempPath;
|
||||
|
||||
public InstallLoop(string InstallLocation, string WimImageId, string TempPath)
|
||||
public InstallLoop(string installLocation, string wimImageId, string tempPath, Action completeCallback, Uri forcedServerUrl)
|
||||
{
|
||||
this.InstallLocation = InstallLocation;
|
||||
this.WimImageId = WimImageId;
|
||||
this.TempPath = TempPath;
|
||||
this.installLocation = installLocation;
|
||||
this.wimImageId = wimImageId;
|
||||
this.tempPath = tempPath;
|
||||
this.completeCallback = completeCallback;
|
||||
this.forcedServerUrl = forcedServerUrl;
|
||||
}
|
||||
|
||||
public void Start(CompleteCallback Callback)
|
||||
public void Start()
|
||||
{
|
||||
mCompleteCallback = Callback;
|
||||
LoopThread = new Thread(new ThreadStart(loopHost));
|
||||
LoopThread.Start();
|
||||
}
|
||||
private void loopHost()
|
||||
var cancellationToken = cancellationTokenSource.Token;
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
//Program.Status.UpdateStatus(null, null, "Testing UI");
|
||||
//Program.SleepThread(5000, false);
|
||||
Interop.InstallInterop.Install(InstallLocation, WimImageId, TempPath);
|
||||
if (mCompleteCallback != null)
|
||||
{
|
||||
mCompleteCallback.BeginInvoke(null, null);
|
||||
}
|
||||
await Interop.InstallInterop.Install(installLocation, wimImageId, tempPath, forcedServerUrl, cancellationToken);
|
||||
completeCallback?.BeginInvoke(null, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -45,10 +38,12 @@ namespace Disco.ClientBootstrapper
|
||||
return;
|
||||
if (ex.GetType() == typeof(ThreadInterruptedException))
|
||||
return;
|
||||
if (ex.GetType() == typeof(OperationCanceledException))
|
||||
return;
|
||||
Program.WriteAppError(ex);
|
||||
throw;
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Disco.ClientBootstrapper.Interop
|
||||
{
|
||||
@@ -20,12 +22,12 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
//Remove(StoreName.Root, StoreLocation.LocalMachine, _tempCerts);
|
||||
}
|
||||
}
|
||||
public static void AddTempCerts()
|
||||
public static async Task AddTempCerts(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_tempCerts == null)
|
||||
_tempCerts = new List<string>();
|
||||
|
||||
var inlineCertificateLocation = Program.InlinePath.Value;
|
||||
var inlineCertificateLocation = Path.GetDirectoryName(typeof(Program).Assembly.Location);
|
||||
|
||||
// Root Certificates
|
||||
try
|
||||
@@ -35,6 +37,7 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
{
|
||||
foreach (var certFile in CertFiles)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var cert = new X509Certificate2(File.ReadAllBytes(certFile), "password");
|
||||
var result = Add(StoreName.Root, StoreLocation.LocalMachine, cert);
|
||||
if (result)
|
||||
@@ -42,7 +45,7 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
if (Path.GetFileNameWithoutExtension(certFile).ToLower().Contains("temp"))
|
||||
_tempCerts.Add(cert.SerialNumber);
|
||||
Program.Status.UpdateStatus(null, null, $"Added Root Certificate: {cert.ShortSubjectName()}");
|
||||
Program.SleepThread(500, false);
|
||||
await Program.SleepThread(500, false, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,6 +63,7 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
{
|
||||
foreach (var certFile in CertFiles)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var cert = new X509Certificate2(File.ReadAllBytes(certFile), "password");
|
||||
var result = Add(StoreName.CertificateAuthority, StoreLocation.LocalMachine, cert);
|
||||
if (result)
|
||||
@@ -67,7 +71,7 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
if (Path.GetFileNameWithoutExtension(certFile).ToLower().Contains("temp"))
|
||||
_tempCerts.Add(cert.SerialNumber);
|
||||
Program.Status.UpdateStatus(null, null, $"Added Intermediate Certificate: {cert.ShortSubjectName()}");
|
||||
Program.SleepThread(500, false);
|
||||
await Program.SleepThread(500, false, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +89,7 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
{
|
||||
foreach (var certFile in CertFiles)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var cert = new X509Certificate2(File.ReadAllBytes(certFile), "password", X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);
|
||||
var result = Add(StoreName.My, StoreLocation.LocalMachine, cert);
|
||||
if (result)
|
||||
@@ -92,7 +97,7 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
if (Path.GetFileNameWithoutExtension(certFile).ToLower().Contains("temp"))
|
||||
_tempCerts.Add(cert.SerialNumber);
|
||||
Program.Status.UpdateStatus(null, null, $"Added Host Certificate: {cert.ShortSubjectName()}");
|
||||
Program.SleepThread(500, false);
|
||||
await Program.SleepThread(500, false, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,17 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Disco.ClientBootstrapper.Interop
|
||||
{
|
||||
public static class InstallInterop
|
||||
{
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, MoveFileFlags dwFlags);
|
||||
private static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, MoveFileFlags dwFlags);
|
||||
[Flags]
|
||||
enum MoveFileFlags
|
||||
private enum MoveFileFlags
|
||||
{
|
||||
MOVEFILE_REPLACE_EXISTING = 0x00000001,
|
||||
MOVEFILE_COPY_ALLOWED = 0x00000002,
|
||||
@@ -22,19 +24,19 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
MOVEFILE_FAIL_IF_NOT_TRACKABLE = 0x00000020
|
||||
}
|
||||
|
||||
private static void Install(string RootFilesystemLocation, RegistryKey RootRegistryLocation, string FilesystemInstallLocation, string VirtualRootFilesystemLocation)
|
||||
private static async Task Install(string rootFilesystemLocation, RegistryKey rootRegistryLocation, string filesystemInstallLocation, string virtualRootFilesystemLocation, Uri forcedServerUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
var SourceLocation = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
|
||||
var InstallLocation = Path.Combine(RootFilesystemLocation, FilesystemInstallLocation);
|
||||
var BootstrapperCmdLinePath = Path.Combine(VirtualRootFilesystemLocation, FilesystemInstallLocation, "Disco.ClientBootstrapper.exe");
|
||||
var InstallLocation = Path.Combine(rootFilesystemLocation, filesystemInstallLocation);
|
||||
var BootstrapperCmdLinePath = Path.Combine(virtualRootFilesystemLocation, filesystemInstallLocation, "Disco.ClientBootstrapper.exe");
|
||||
|
||||
var GroupPolicyScriptsIniLocation = Path.Combine(RootFilesystemLocation, "Windows\\System32\\GroupPolicy\\Machine\\Scripts\\scripts.ini");
|
||||
var GroupPolicyScriptsIniBackupLocation = Path.Combine(RootFilesystemLocation, "Windows\\System32\\GroupPolicy\\Machine\\Scripts\\disco_scripts.ini");
|
||||
var GroupPolicyScriptsIniLocation = Path.Combine(rootFilesystemLocation, @"Windows\System32\GroupPolicy\Machine\Scripts\scripts.ini");
|
||||
var GroupPolicyScriptsIniBackupLocation = Path.Combine(rootFilesystemLocation, @"Windows\System32\GroupPolicy\Machine\Scripts\disco_scripts.ini");
|
||||
|
||||
// Create file system Location
|
||||
#region "Create File System Location"
|
||||
Program.Status.UpdateStatus(null, null, "Creating Installation Location");
|
||||
Program.SleepThread(500, false);
|
||||
await Program.SleepThread(500, false, cancellationToken);
|
||||
if (Directory.Exists(InstallLocation))
|
||||
{
|
||||
// Try and Delete Directory
|
||||
@@ -52,19 +54,23 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
var installDir = Directory.CreateDirectory(InstallLocation);
|
||||
installDir.Attributes = installDir.Attributes | FileAttributes.Hidden;
|
||||
}
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
#endregion
|
||||
|
||||
// Copy files to file system location
|
||||
#region "Copy to File System"
|
||||
Program.Status.UpdateStatus(null, null, "Copying Files");
|
||||
Program.SleepThread(500, false);
|
||||
await Program.SleepThread(500, false, cancellationToken);
|
||||
|
||||
// Copy Bootstrapper
|
||||
// ie: Executing Assembly
|
||||
File.Copy(System.Reflection.Assembly.GetExecutingAssembly().Location, Path.Combine(InstallLocation, "Disco.ClientBootstrapper.exe"));
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
foreach (var file in Directory.EnumerateFiles(SourceLocation))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var fileName = Path.GetFileName(file);
|
||||
|
||||
// Only Copy Certain Files
|
||||
@@ -86,7 +92,7 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
// Backup & Create Group Policy Scripts.ini
|
||||
#region "Group Policy Scripts.ini"
|
||||
Program.Status.UpdateStatus(null, null, "Creating Group Policy Script Entry");
|
||||
Program.SleepThread(500, false);
|
||||
await Program.SleepThread(500, false, cancellationToken);
|
||||
// Backup
|
||||
if (!File.Exists(GroupPolicyScriptsIniBackupLocation))
|
||||
{
|
||||
@@ -95,6 +101,7 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
File.Move(GroupPolicyScriptsIniLocation, GroupPolicyScriptsIniBackupLocation);
|
||||
}
|
||||
}
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Create
|
||||
if (File.Exists(GroupPolicyScriptsIniLocation))
|
||||
@@ -105,56 +112,67 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
{
|
||||
using (var scriptsIniStreamWriter = new StreamWriter(scriptsIniStream, Encoding.Unicode))
|
||||
{
|
||||
scriptsIniStreamWriter.Write($"[Startup]{Environment.NewLine}0CmdLine={BootstrapperCmdLinePath}{Environment.NewLine}0Parameters=/AllowUninstall");
|
||||
scriptsIniStreamWriter.Flush();
|
||||
scriptsIniStreamWriter.WriteLine("[Startup]");
|
||||
scriptsIniStreamWriter.WriteLine($"0CmdLine={BootstrapperCmdLinePath}");
|
||||
if (forcedServerUrl == null)
|
||||
scriptsIniStreamWriter.WriteLine("0Parameters=/AllowUninstall");
|
||||
else
|
||||
scriptsIniStreamWriter.WriteLine($"0Parameters=/AllowUninstall {forcedServerUrl}");
|
||||
}
|
||||
}
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
#endregion
|
||||
|
||||
// Backup & Create Group Policy Registry
|
||||
#region "Group Policy Registry"
|
||||
Program.Status.UpdateStatus(null, null, "Creating Group Policy Registry Entries");
|
||||
Program.SleepThread(500, false);
|
||||
await Program.SleepThread(500, false, cancellationToken);
|
||||
// Backup Scripts
|
||||
using (var regGroupPolicy = RootRegistryLocation.OpenSubKey("Microsoft\\Windows\\CurrentVersion\\Group Policy", true))
|
||||
using (var regGroupPolicy = rootRegistryLocation.OpenSubKey(@"Microsoft\Windows\CurrentVersion\Group Policy", true))
|
||||
{
|
||||
if (regGroupPolicy != null && regGroupPolicy.GetSubKeyNames().Contains("Scripts") && !regGroupPolicy.GetSubKeyNames().Contains("Disco_Scripts"))
|
||||
{
|
||||
RegistryUtilities.RenameSubKey(regGroupPolicy, "Scripts", "Disco_Scripts");
|
||||
}
|
||||
}
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Create Scripts
|
||||
RootRegistryLocation.CreateSubKey("Microsoft\\Windows\\CurrentVersion\\Group Policy\\Scripts\\Shutdown").Dispose();
|
||||
using (var regScriptsStartup = RootRegistryLocation.CreateSubKey("Microsoft\\Windows\\CurrentVersion\\Group Policy\\Scripts\\Startup\\0"))
|
||||
rootRegistryLocation.CreateSubKey(@"Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Shutdown").Dispose();
|
||||
using (var regScriptsStartup = rootRegistryLocation.CreateSubKey(@"Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Startup\0"))
|
||||
{
|
||||
regScriptsStartup.SetValue("GPO-ID", "LocalGPO", RegistryValueKind.String);
|
||||
regScriptsStartup.SetValue("SOM-ID", "Local", RegistryValueKind.String);
|
||||
regScriptsStartup.SetValue("FileSysPath", Path.Combine(Environment.SystemDirectory, "GroupPolicy\\Machine"), RegistryValueKind.String);
|
||||
regScriptsStartup.SetValue("FileSysPath", Path.Combine(Environment.SystemDirectory, @"GroupPolicy\Machine"), RegistryValueKind.String);
|
||||
regScriptsStartup.SetValue("DisplayName", "Local Group Policy", RegistryValueKind.String);
|
||||
regScriptsStartup.SetValue("GPOName", "Local Group Policy", RegistryValueKind.String);
|
||||
regScriptsStartup.SetValue("PSScriptOrder", 1, RegistryValueKind.DWord);
|
||||
using (var regScriptsStartup0 = regScriptsStartup.CreateSubKey("0"))
|
||||
{
|
||||
regScriptsStartup0.SetValue("Script", BootstrapperCmdLinePath, RegistryValueKind.String);
|
||||
if (forcedServerUrl == null)
|
||||
regScriptsStartup0.SetValue("Parameters", "/AllowUninstall", RegistryValueKind.String);
|
||||
else
|
||||
regScriptsStartup0.SetValue("Parameters", $"/AllowUninstall {forcedServerUrl}", RegistryValueKind.String);
|
||||
regScriptsStartup0.SetValue("IsPowershell", 0, RegistryValueKind.DWord);
|
||||
regScriptsStartup0.SetValue("ExecTime", new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, RegistryValueKind.Binary);
|
||||
}
|
||||
}
|
||||
RootRegistryLocation.CreateSubKey("Microsoft\\Windows\\CurrentVersion\\Group Policy\\State\\Machine\\Scripts\\Shutdown").Dispose();
|
||||
rootRegistryLocation.CreateSubKey(@"Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\Shutdown").Dispose();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Backup Scripts State
|
||||
using (var regGroupPolicy = RootRegistryLocation.OpenSubKey("Microsoft\\Windows\\CurrentVersion\\Group Policy\\State\\Machine", true))
|
||||
using (var regGroupPolicy = rootRegistryLocation.OpenSubKey(@"Microsoft\Windows\CurrentVersion\Group Policy\State\Machine", true))
|
||||
{
|
||||
if (regGroupPolicy != null && regGroupPolicy.GetSubKeyNames().Contains("Scripts") && !regGroupPolicy.GetSubKeyNames().Contains("Disco_Scripts"))
|
||||
{
|
||||
RegistryUtilities.RenameSubKey(regGroupPolicy, "Scripts", "Disco_Scripts");
|
||||
}
|
||||
}
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Create Scripts State
|
||||
using (var regStateScriptsStartup = RootRegistryLocation.CreateSubKey("Microsoft\\Windows\\CurrentVersion\\Group Policy\\State\\Machine\\Scripts\\Startup\\0"))
|
||||
using (var regStateScriptsStartup = rootRegistryLocation.CreateSubKey(@"Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\Startup\0"))
|
||||
{
|
||||
regStateScriptsStartup.SetValue("GPO-ID", "LocalGPO", RegistryValueKind.String);
|
||||
regStateScriptsStartup.SetValue("SOM-ID", "Local", RegistryValueKind.String);
|
||||
@@ -165,17 +183,21 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
using (var regStateScriptsStartup0 = regStateScriptsStartup.CreateSubKey("0"))
|
||||
{
|
||||
regStateScriptsStartup0.SetValue("Script", BootstrapperCmdLinePath, RegistryValueKind.String);
|
||||
if (forcedServerUrl == null)
|
||||
regStateScriptsStartup0.SetValue("Parameters", "/AllowUninstall", RegistryValueKind.String);
|
||||
else
|
||||
regStateScriptsStartup0.SetValue("Parameters", $"/AllowUninstall {forcedServerUrl}", RegistryValueKind.String);
|
||||
regStateScriptsStartup0.SetValue("ExecTime", new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, RegistryValueKind.Binary);
|
||||
}
|
||||
}
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
#endregion
|
||||
|
||||
// Set Registry Startup Environment Policies
|
||||
#region "Registry Startup Policies"
|
||||
Program.Status.UpdateStatus(null, null, "Creating Startup Policy Registry Entries");
|
||||
Program.SleepThread(500, false);
|
||||
using (var regWinlogon = RootRegistryLocation.OpenSubKey("Microsoft\\Windows NT\\CurrentVersion\\Winlogon", true))
|
||||
await Program.SleepThread(500, false, cancellationToken);
|
||||
using (var regWinlogon = rootRegistryLocation.OpenSubKey(@"Microsoft\Windows NT\CurrentVersion\Winlogon", true))
|
||||
{
|
||||
regWinlogon.SetValue("HideStartupScripts", 0, RegistryValueKind.DWord);
|
||||
regWinlogon.SetValue("RunStartupScriptSync", 1, RegistryValueKind.DWord);
|
||||
@@ -183,96 +205,112 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
#endregion
|
||||
}
|
||||
|
||||
public static void Install(string InstallLocation, string WimImageId, string TempPath)
|
||||
public static async Task Install(string installLocation, string wimImageId, string tempPath, Uri forcedServerUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
Program.Status.UpdateStatus("Installing Bootstrapper", "Starting", "Please wait...", false);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(InstallLocation))
|
||||
InstallLocation = Path.Combine(Path.GetPathRoot(Environment.SystemDirectory), "Disco");
|
||||
if (string.IsNullOrWhiteSpace(installLocation))
|
||||
installLocation = Path.Combine(Path.GetPathRoot(Environment.SystemDirectory), "Disco");
|
||||
|
||||
if (InstallLocation.EndsWith(".wim", StringComparison.OrdinalIgnoreCase))
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (installLocation.EndsWith(".wim", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Offline File System (WIM)
|
||||
Program.Status.UpdateStatus("Installing Bootstrapper (Offline)", "Installing", $"Install Location: {InstallLocation}");
|
||||
Program.SleepThread(1000, false);
|
||||
Program.Status.UpdateStatus("Installing Bootstrapper (Offline)", "Installing", $"Install Location: {installLocation}");
|
||||
await Program.SleepThread(1000, false, cancellationToken);
|
||||
|
||||
// Mount WIM
|
||||
int wimImageIndex = 0;
|
||||
using (var wim = new WIMInterop.WindowsImageContainer(InstallLocation, WIMInterop.WindowsImageContainer.CreateFileMode.OpenExisting, WIMInterop.WindowsImageContainer.CreateFileAccess.Write))
|
||||
using (var wim = new WIMInterop.WindowsImageContainer(installLocation, WIMInterop.WindowsImageContainer.CreateFileMode.OpenExisting, WIMInterop.WindowsImageContainer.CreateFileAccess.Write))
|
||||
{
|
||||
if (WimImageId == null)
|
||||
WimImageId = "1";
|
||||
if (!int.TryParse(WimImageId, out wimImageIndex))
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (wimImageId == null)
|
||||
wimImageId = "1";
|
||||
if (!int.TryParse(wimImageId, out wimImageIndex))
|
||||
{
|
||||
Program.Status.UpdateStatus(null, "Analysing WIM", $"Looking for Image Name: {WimImageId}");
|
||||
Program.SleepThread(500, false);
|
||||
Program.Status.UpdateStatus(null, "Analysing WIM", $"Looking for Image Name: {wimImageId}");
|
||||
await Program.SleepThread(500, false, cancellationToken);
|
||||
for (int i = 0; i < wim.ImageCount; i++)
|
||||
{
|
||||
var wimImageInfo = new System.Xml.XmlDocument();
|
||||
using (var wimImage = wim[i])
|
||||
wimImageInfo.LoadXml(wimImage.ImageInformation);
|
||||
var wimImageInfoName = wimImageInfo.SelectSingleNode("//IMAGE/NAME");
|
||||
if (wimImageInfoName != null && wimImageInfoName.InnerText.Equals(WimImageId, StringComparison.OrdinalIgnoreCase))
|
||||
if (wimImageInfoName != null && wimImageInfoName.InnerText.Equals(wimImageId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
wimImageIndex = i + 1;
|
||||
Program.Status.UpdateStatus(null, "Analysing WIM", $"Found Image Id '{WimImageId}' at Index {wimImageIndex}");
|
||||
Program.SleepThread(500, false);
|
||||
Program.Status.UpdateStatus(null, "Analysing WIM", $"Found Image Id '{wimImageId}' at Index {wimImageIndex}");
|
||||
await Program.SleepThread(500, false, cancellationToken);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (wimImageIndex == 0)
|
||||
{
|
||||
Program.Status.UpdateStatus(null, "Error", $"Unable to load WIM Image Id: {WimImageId}");
|
||||
Program.SleepThread(5000, false);
|
||||
Program.Status.UpdateStatus(null, "Error", $"Unable to load WIM Image Id: {wimImageId}");
|
||||
await Program.SleepThread(5000, false, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get Temp Path
|
||||
var wimMountPath = Path.Combine(TempPath ?? Path.GetTempPath(), "DiscoClientBootstrapperWimMount");
|
||||
var wimMountPath = Path.Combine(tempPath ?? Path.GetTempPath(), "DiscoClientBootstrapperWimMount");
|
||||
if (Directory.Exists(wimMountPath))
|
||||
Directory.Delete(wimMountPath, true);
|
||||
Directory.CreateDirectory(wimMountPath);
|
||||
|
||||
var wimTempMountPath = Path.Combine(TempPath ?? Path.GetTempPath(), "DiscoClientBootstrapperWimTempMount");
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var wimTempMountPath = Path.Combine(tempPath ?? Path.GetTempPath(), "DiscoClientBootstrapperWimTempMount");
|
||||
if (Directory.Exists(wimTempMountPath))
|
||||
Directory.Delete(wimTempMountPath, true);
|
||||
Directory.CreateDirectory(wimTempMountPath);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
bool wimCommitChanges = true;
|
||||
WIMInterop.WindowsImageContainer.NativeMethods.MessageCallback m_MessageCallback = null;
|
||||
try
|
||||
{
|
||||
// Mount WIM
|
||||
Program.Status.UpdateStatus(null, "Mounting WIM", $"Mounting WIM Image to '{wimMountPath}'");
|
||||
Program.SleepThread(500, false);
|
||||
await Program.SleepThread(500, false, cancellationToken);
|
||||
m_MessageCallback = new WIMInterop.WindowsImageContainer.NativeMethods.MessageCallback(WimImageEventMessagePump);
|
||||
WIMInterop.WindowsImageContainer.NativeMethods.RegisterCallback(m_MessageCallback);
|
||||
|
||||
WIMInterop.WindowsImageContainer.NativeMethods.MountImage(wimMountPath, InstallLocation, wimImageIndex, wimTempMountPath);
|
||||
WIMInterop.WindowsImageContainer.NativeMethods.MountImage(wimMountPath, installLocation, wimImageIndex, wimTempMountPath);
|
||||
|
||||
// Load Local Machine Registry
|
||||
var wimHivePath = Path.Combine(wimMountPath, "Windows\\System32\\config\\SOFTWARE");
|
||||
Program.Status.UpdateStatus(null, "Mounting Offline Registry Hive", $"Mounting Offline Registry Hive at '{wimHivePath}'");
|
||||
Program.SleepThread(500, false);
|
||||
await Program.SleepThread(500, false, cancellationToken);
|
||||
using (var wimReg = new RegistryInterop(RegistryInterop.RegistryHives.HKEY_LOCAL_MACHINE, "DiscoClientBootstrapperWimHive", wimHivePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
using (RegistryKey rootRegistryLocation = Registry.LocalMachine.OpenSubKey("DiscoClientBootstrapperWimHive", true))
|
||||
{
|
||||
string rootFileSystemLocation = wimMountPath;
|
||||
string fileSystemInstallLocation = "Disco";
|
||||
string virtualRootFileSystemLocation = "C:\\";
|
||||
|
||||
Install(rootFileSystemLocation, rootRegistryLocation, fileSystemInstallLocation, virtualRootFileSystemLocation);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await Install(rootFileSystemLocation, rootRegistryLocation, fileSystemInstallLocation, virtualRootFileSystemLocation, forcedServerUrl, cancellationToken);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Unload Local Machine Registry
|
||||
Program.Status.UpdateStatus(null, "Unmounting Offline Registry Hive", $"Unmounting Offline Registry Hive at '{wimHivePath}'");
|
||||
Program.SleepThread(500, false);
|
||||
await Program.SleepThread(500, false, cancellationToken);
|
||||
wimReg.Unload();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
wimCommitChanges = false;
|
||||
@@ -282,8 +320,8 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
{
|
||||
// Unmount WIM
|
||||
Program.Status.UpdateStatus(null, "Unmounting WIM", $"Unmounting WIM Image at '{wimMountPath}'");
|
||||
Program.SleepThread(500, false);
|
||||
WIMInterop.WindowsImageContainer.NativeMethods.DismountImage(wimMountPath, InstallLocation, wimImageIndex, wimCommitChanges);
|
||||
await Program.SleepThread(500, false, cancellationToken);
|
||||
WIMInterop.WindowsImageContainer.NativeMethods.DismountImage(wimMountPath, installLocation, wimImageIndex, wimCommitChanges);
|
||||
|
||||
if (m_MessageCallback != null)
|
||||
{
|
||||
@@ -295,23 +333,25 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
Directory.Delete(wimMountPath, true);
|
||||
if (Directory.Exists(wimTempMountPath))
|
||||
Directory.Delete(wimTempMountPath, true);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Online File System
|
||||
Program.Status.UpdateStatus("Installing Bootstrapper (Online)", "Installing", $"Install Location: {InstallLocation}", true, -1);
|
||||
Program.SleepThread(1000, false);
|
||||
string rootFileSystemLocation = Path.GetPathRoot(InstallLocation);
|
||||
Program.Status.UpdateStatus("Installing Bootstrapper (Online)", "Installing", $"Install Location: {installLocation}", true, -1);
|
||||
await Program.SleepThread(1000, false, cancellationToken);
|
||||
string rootFileSystemLocation = Path.GetPathRoot(installLocation);
|
||||
RegistryKey rootRegistryLocation = Registry.LocalMachine.OpenSubKey("SOFTWARE", true);
|
||||
string fileSystemInstallLocation = InstallLocation.Substring(rootFileSystemLocation.Length);
|
||||
string fileSystemInstallLocation = installLocation.Substring(rootFileSystemLocation.Length);
|
||||
|
||||
Install(rootFileSystemLocation, rootRegistryLocation, fileSystemInstallLocation, rootFileSystemLocation);
|
||||
await Install(rootFileSystemLocation, rootRegistryLocation, fileSystemInstallLocation, rootFileSystemLocation, forcedServerUrl, cancellationToken);
|
||||
Program.Status.UpdateStatus(null, "Online File System Installation Complete", string.Empty, true, -1);
|
||||
Program.SleepThread(1000, false);
|
||||
await Program.SleepThread(1000, false, cancellationToken);
|
||||
}
|
||||
Program.Status.UpdateStatus(null, "Complete", "Finished Installing Bootstrapper");
|
||||
Program.SleepThread(1500, false);
|
||||
await Program.SleepThread(1500, false, cancellationToken);
|
||||
}
|
||||
|
||||
private static uint WimImageEventMessagePump(
|
||||
@@ -349,41 +389,28 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
return status;
|
||||
}
|
||||
|
||||
public static void Uninstall()
|
||||
public static async Task Uninstall(CancellationToken cancellationToken)
|
||||
{
|
||||
// Application Directory
|
||||
var appDirectory = Program.InlinePath.Value;
|
||||
if (Program.AllowUninstall && !appDirectory.StartsWith("\\\\"))
|
||||
var appDirectory = Path.GetDirectoryName(typeof(Program).Assembly.Location);
|
||||
if (Program.AllowUninstall && !appDirectory.StartsWith(@"\\"))
|
||||
{
|
||||
Program.Status.UpdateStatus("System Preparation (Bootstrapper)", "Uninstalling Bootstrapper...", string.Empty, false, 0);
|
||||
Program.SleepThread(1000, true);
|
||||
//var uninstallScriptLocation = System.IO.Path.Combine(appDirectory, "UninstallBootstrapper.vbs");
|
||||
//if (System.IO.File.Exists(uninstallScriptLocation))
|
||||
//{
|
||||
// var bootstrapperPID = System.Diagnostics.Process.GetCurrentProcess().Id;
|
||||
// var cscriptPath = System.IO.Path.Combine(Environment.SystemDirectory, "cscript.exe");
|
||||
// var cscriptArgs = string.Format("\"{0}\" /WaitForProcessID:{1}", uninstallScriptLocation, bootstrapperPID);
|
||||
|
||||
// var startProc = new ProcessStartInfo(cscriptPath, cscriptArgs);
|
||||
// startProc.WorkingDirectory = Environment.SystemDirectory;
|
||||
// startProc.WindowStyle = ProcessWindowStyle.Hidden;
|
||||
|
||||
// Process.Start(startProc);
|
||||
//}
|
||||
await Program.SleepThread(1000, true, cancellationToken);
|
||||
|
||||
// Remove Registry Entries
|
||||
using (var regWinlogon = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon", true))
|
||||
using (var regWinlogon = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon", true))
|
||||
{
|
||||
regWinlogon.DeleteValue("HideStartupScripts", false);
|
||||
regWinlogon.DeleteValue("RunStartupScriptSync", false);
|
||||
}
|
||||
Registry.LocalMachine.DeleteSubKeyTree("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\Scripts\\Shutdown", false);
|
||||
Registry.LocalMachine.DeleteSubKeyTree("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\Scripts\\Startup", false);
|
||||
Registry.LocalMachine.DeleteSubKeyTree("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\State\\Machine\\Scripts\\Shutdown", false);
|
||||
Registry.LocalMachine.DeleteSubKeyTree("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\State\\Machine\\Scripts\\Startup", false);
|
||||
Registry.LocalMachine.DeleteSubKeyTree(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Shutdown", false);
|
||||
Registry.LocalMachine.DeleteSubKeyTree(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Startup", false);
|
||||
Registry.LocalMachine.DeleteSubKeyTree(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\Shutdown", false);
|
||||
Registry.LocalMachine.DeleteSubKeyTree(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\Startup", false);
|
||||
|
||||
// Restore Registry Backups
|
||||
using (var regGroupPolicy = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy", true))
|
||||
using (var regGroupPolicy = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy", true))
|
||||
{
|
||||
if (regGroupPolicy != null && regGroupPolicy.GetSubKeyNames().Contains("Disco_Scripts"))
|
||||
{
|
||||
@@ -391,7 +418,7 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
RegistryUtilities.RenameSubKey(regGroupPolicy, "Disco_Scripts", "Scripts");
|
||||
}
|
||||
}
|
||||
using (var regGroupPolicy = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\State\\Machine", true))
|
||||
using (var regGroupPolicy = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine", true))
|
||||
{
|
||||
if (regGroupPolicy != null && regGroupPolicy.GetSubKeyNames().Contains("Disco_Scripts"))
|
||||
{
|
||||
@@ -401,10 +428,10 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
}
|
||||
|
||||
// Delete Group Policy Script File
|
||||
var groupPolicyScriptsPath = Path.Combine(Environment.SystemDirectory, "GroupPolicy\\Machine\\Scripts\\scripts.ini");
|
||||
var groupPolicyScriptsPath = Path.Combine(Environment.SystemDirectory, @"GroupPolicy\Machine\Scripts\scripts.ini");
|
||||
if (File.Exists(groupPolicyScriptsPath))
|
||||
File.Delete(groupPolicyScriptsPath);
|
||||
var groupPolicyScriptsBackupPath = Path.Combine(Environment.SystemDirectory, "GroupPolicy\\Machine\\Scripts\\disco_scripts.ini");
|
||||
var groupPolicyScriptsBackupPath = Path.Combine(Environment.SystemDirectory, @"GroupPolicy\Machine\Scripts\disco_scripts.ini");
|
||||
if (File.Exists(groupPolicyScriptsBackupPath))
|
||||
File.Move(groupPolicyScriptsBackupPath, groupPolicyScriptsPath);
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Management;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
|
||||
namespace Disco.ClientBootstrapper.Interop
|
||||
{
|
||||
static class NetworkInterop
|
||||
internal static class NetworkInterop
|
||||
{
|
||||
|
||||
#region PInvoke
|
||||
@@ -164,30 +167,35 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
}
|
||||
}
|
||||
|
||||
public static bool PingDiscoIct(string ServerName)
|
||||
public static bool HasNetworkConnectivity()
|
||||
{
|
||||
using (Ping p = new Ping())
|
||||
var nics = NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(ni => ni.OperationalStatus == OperationalStatus.Up)
|
||||
.ToList();
|
||||
|
||||
foreach (var nic in nics)
|
||||
{
|
||||
try
|
||||
if (nic.Supports(NetworkInterfaceComponent.IPv4))
|
||||
{
|
||||
PingReply pr = p.Send(ServerName, 2000);
|
||||
if (pr.Status == IPStatus.Success)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var ipProps = nic.GetIPProperties();
|
||||
var ipv4Props = ipProps.GetIPv4Properties();
|
||||
if (ipv4Props.IsAutomaticPrivateAddressingActive)
|
||||
continue;
|
||||
|
||||
return ipProps.UnicastAddresses
|
||||
.Where(ua => ua.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
|
||||
.Any();
|
||||
}
|
||||
}
|
||||
|
||||
public static void ConfigureWireless()
|
||||
return false;
|
||||
}
|
||||
|
||||
public static async Task ConfigureWireless(CancellationToken cancellationToken)
|
||||
{
|
||||
// Add Certificates
|
||||
Program.Status.UpdateStatus(null, null, "Configuring Wireless Certificates");
|
||||
CertificateInterop.AddTempCerts();
|
||||
await CertificateInterop.AddTempCerts(cancellationToken);
|
||||
|
||||
// Add Wireless Profiles
|
||||
Program.Status.UpdateStatus(null, null, "Configuring Wireless Profiles");
|
||||
@@ -208,15 +216,16 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
{
|
||||
foreach (var inlineWirelessProfile in wirelessInlineProfiles)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (inlineWirelessProfile.AddProfile(wlanHandle, na.Guid))
|
||||
{
|
||||
Program.Status.UpdateStatus(null, null, $"Added Wireless Profile: {inlineWirelessProfile.ProfileName}");
|
||||
Program.SleepThread(500, false);
|
||||
await Program.SleepThread(500, false, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
Program.Status.UpdateStatus(null, null, $"Unable to add Wireless Profile: {inlineWirelessProfile.ProfileName}");
|
||||
Program.SleepThread(5000, false);
|
||||
await Program.SleepThread(5000, false, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,14 +255,15 @@ namespace Disco.ClientBootstrapper.Interop
|
||||
|
||||
private static List<WirelessProfile> GetInlineWirelessProfiles()
|
||||
{
|
||||
var inlineProfileFiles = System.IO.Directory.EnumerateFiles(Program.InlinePath.Value, "WLAN_Profile_*.xml").ToList();
|
||||
var directoryPath = Path.GetDirectoryName(typeof(Program).Assembly.Location);
|
||||
var inlineProfileFiles = Directory.EnumerateFiles(directoryPath, "WLAN_Profile_*.xml").ToList();
|
||||
var inlineProfiles = new List<WirelessProfile>(inlineProfileFiles.Count);
|
||||
foreach (var filename in inlineProfileFiles)
|
||||
{
|
||||
var profile = new WirelessProfile()
|
||||
{
|
||||
Filename = filename,
|
||||
ProfileXml = System.IO.File.ReadAllText(filename)
|
||||
ProfileXml = File.ReadAllText(filename)
|
||||
};
|
||||
var profileXml = new XmlDocument();
|
||||
profileXml.LoadXml(profile.ProfileXml);
|
||||
|
||||
@@ -1,36 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Disco.ClientBootstrapper
|
||||
{
|
||||
static class Program
|
||||
internal static class Program
|
||||
{
|
||||
public static IStatus Status { get; set; }
|
||||
public static BootstrapperLoop BootstrapperLoop { get; set; }
|
||||
public static InstallLoop InstallLoop { get; set; }
|
||||
private static readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
|
||||
public static IStatus Status { get; private set; }
|
||||
|
||||
public static List<string> PostBootstrapperActions { get; set; }
|
||||
public static bool AllowUninstall { get; set; }
|
||||
public static bool ApplicationExiting { get; set; }
|
||||
public static Lazy<string> InlinePath = new Lazy<string>(() =>
|
||||
{
|
||||
return System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
|
||||
});
|
||||
public static bool AllowUninstall { get; private set; }
|
||||
public static Uri ForcedServerUrl { get; private set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// The main entry point for the application.
|
||||
/// </summary>
|
||||
[STAThread]
|
||||
static void Main(string[] args)
|
||||
private static void Main(string[] args)
|
||||
{
|
||||
Application.ThreadException += new ThreadExceptionEventHandler(Application_ThreadException);
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
|
||||
|
||||
|
||||
if (args.Length > 0)
|
||||
{
|
||||
#if DEBUG
|
||||
if (args.Any(a => a.Equals("debug", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
do
|
||||
{
|
||||
Console.WriteLine("Waiting for Debugger to Attach");
|
||||
Thread.Sleep(1000);
|
||||
} while (!System.Diagnostics.Debugger.IsAttached);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (args.Any(a => a.StartsWith("http://", StringComparison.OrdinalIgnoreCase)))
|
||||
throw new ArgumentException("Only HTTPS URLs are supported for a forced server URL.");
|
||||
var forcedServerArg = args.FirstOrDefault(a => a.StartsWith("https://", StringComparison.OrdinalIgnoreCase));
|
||||
if (forcedServerArg != null)
|
||||
{
|
||||
if (Uri.TryCreate(forcedServerArg, UriKind.Absolute, out var forcedUri))
|
||||
ForcedServerUrl = forcedUri;
|
||||
else
|
||||
throw new ArgumentException("The provided forced server URL is not valid.");
|
||||
}
|
||||
|
||||
switch (args[0].ToLower())
|
||||
{
|
||||
case "/install":
|
||||
@@ -46,14 +68,17 @@ namespace Disco.ClientBootstrapper
|
||||
wimImage = args[2];
|
||||
if (args.Length > 3)
|
||||
tempPath = args[3];
|
||||
InstallLoop = new InstallLoop(installLocation, wimImage, tempPath);
|
||||
InstallLoop.Start(new InstallLoop.CompleteCallback(InstallComplete));
|
||||
var installLoop = new InstallLoop(installLocation, wimImage, tempPath, InstallComplete, ForcedServerUrl);
|
||||
installLoop.Start();
|
||||
Application.Run();
|
||||
return;
|
||||
case "/uninstall":
|
||||
AllowUninstall = true;
|
||||
Status = new NullStatus();
|
||||
Interop.InstallInterop.Uninstall();
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Interop.InstallInterop.Uninstall(cancellationTokenSource.Token);
|
||||
}).Wait(cancellationTokenSource.Token);
|
||||
return;
|
||||
case "/allowuninstall":
|
||||
AllowUninstall = true;
|
||||
@@ -71,13 +96,13 @@ namespace Disco.ClientBootstrapper
|
||||
statusForm.Show();
|
||||
}
|
||||
|
||||
BootstrapperLoop = new BootstrapperLoop(Status, new BootstrapperLoop.LoopCompleteCallback(LoopComplete));
|
||||
BootstrapperLoop.Start();
|
||||
var bootstrapperLoop = new BootstrapperLoop(Status, ForcedServerUrl, LoopComplete, cancellationTokenSource.Token);
|
||||
bootstrapperLoop.Start();
|
||||
|
||||
Application.Run();
|
||||
}
|
||||
|
||||
static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
|
||||
private static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
|
||||
{
|
||||
WriteAppError(e.Exception);
|
||||
}
|
||||
@@ -100,7 +125,7 @@ namespace Disco.ClientBootstrapper
|
||||
catch (Exception) { }
|
||||
}
|
||||
|
||||
public static void LoopComplete()
|
||||
public static async Task LoopComplete(CancellationToken cancellationToken)
|
||||
{
|
||||
// Run Post Actions
|
||||
if (PostBootstrapperActions != null)
|
||||
@@ -108,32 +133,32 @@ namespace Disco.ClientBootstrapper
|
||||
// Check Uninstall
|
||||
if (AllowUninstall && PostBootstrapperActions.Contains("UninstallBootstrapper"))
|
||||
{
|
||||
Interop.InstallInterop.Uninstall();
|
||||
await Interop.InstallInterop.Uninstall(cancellationToken);
|
||||
}
|
||||
|
||||
// Check ShutdownActions
|
||||
if (PostBootstrapperActions.Contains("Shutdown"))
|
||||
{
|
||||
Status.UpdateStatus("System Preparation (Bootstrapper)", "Shutting Down; Finished...", string.Empty, false, 0);
|
||||
SleepThread(4000, true);
|
||||
await SleepThread(4000, true, cancellationToken);
|
||||
Interop.ShutdownInterop.Shutdown();
|
||||
}
|
||||
else if (PostBootstrapperActions.Contains("Reboot"))
|
||||
{
|
||||
Status.UpdateStatus("System Preparation (Bootstrapper)", "Rebooting; Finished...", string.Empty, false, 0);
|
||||
SleepThread(4000, true);
|
||||
await SleepThread(4000, true, cancellationToken);
|
||||
Interop.ShutdownInterop.Reboot();
|
||||
}
|
||||
else
|
||||
{
|
||||
Status.UpdateStatus("System Preparation (Bootstrapper)", "Starting System; Finished...", string.Empty, false, 0);
|
||||
SleepThread(2000, true);
|
||||
await SleepThread(2000, true, cancellationToken);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Status.UpdateStatus("System Preparation (Bootstrapper)", "Starting System; Finished...", string.Empty, false, 0);
|
||||
SleepThread(2000, true);
|
||||
await SleepThread(2000, true, cancellationToken);
|
||||
}
|
||||
|
||||
ExitApplication();
|
||||
@@ -146,33 +171,12 @@ namespace Disco.ClientBootstrapper
|
||||
|
||||
public static void ExitApplication()
|
||||
{
|
||||
if (!ApplicationExiting)
|
||||
{
|
||||
ApplicationExiting = true;
|
||||
if (BootstrapperLoop != null)
|
||||
{
|
||||
if (BootstrapperLoop.LoopThread != null)
|
||||
{
|
||||
if (BootstrapperLoop.LoopThread.ThreadState == ThreadState.WaitSleepJoin)
|
||||
{
|
||||
BootstrapperLoop.LoopThread.Interrupt();
|
||||
}
|
||||
if (BootstrapperLoop.LoopThread.ThreadState == ThreadState.Running)
|
||||
{
|
||||
BootstrapperLoop.LoopThread.Abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!cancellationTokenSource.IsCancellationRequested)
|
||||
cancellationTokenSource.Cancel();
|
||||
Application.Exit();
|
||||
}
|
||||
}
|
||||
|
||||
public static void Trace(string Format, params string[] args)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(Format, args);
|
||||
}
|
||||
|
||||
public static void SleepThread(int millisecondsTimeout, bool updateUI)
|
||||
public static async Task SleepThread(int millisecondsTimeout, bool updateUI, CancellationToken cancellationToken)
|
||||
{
|
||||
if (updateUI)
|
||||
{
|
||||
@@ -180,12 +184,12 @@ namespace Disco.ClientBootstrapper
|
||||
{
|
||||
int progress = Convert.ToInt32(((Convert.ToDouble(i) / Convert.ToDouble(millisecondsTimeout)) * 100));
|
||||
Status.UpdateStatus(null, null, null, true, progress);
|
||||
Thread.Sleep(500);
|
||||
await Task.Delay(500, cancellationToken);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Thread.Sleep(millisecondsTimeout);
|
||||
await Task.Delay(millisecondsTimeout, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,5 +14,11 @@ namespace Disco.Data.Configuration.Modules
|
||||
get => Get(DeviceExportOptions.DefaultOptions());
|
||||
set => Set(value);
|
||||
}
|
||||
|
||||
public bool EnrollmentLegacyDiscoveryDisabled
|
||||
{
|
||||
get => Get(false);
|
||||
set => Set(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
<Compile Include="Repository\FlagType.cs" />
|
||||
<Compile Include="Repository\User\UserComment.cs" />
|
||||
<Compile Include="Repository\FlagPermission.cs" />
|
||||
<Compile Include="Services\Devices\DeviceEnrolmentServerDiscoveryMethod.cs" />
|
||||
<Compile Include="Services\Devices\DeviceFlags\DeviceFlagExportOptions.cs" />
|
||||
<Compile Include="Services\Devices\DeviceFlags\DeviceFlagExportRecord.cs" />
|
||||
<Compile Include="Services\Documents\DocumentExportOptions.cs" />
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Disco.Models.Services.Devices
|
||||
{
|
||||
public enum DeviceEnrolmentServerDiscoveryMethod
|
||||
{
|
||||
Unknown = 0,
|
||||
Manual = 1,
|
||||
SRV = 2,
|
||||
VicSmart = 3,
|
||||
Legacy = 4,
|
||||
Mac = 50,
|
||||
MacSecure = 51,
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ namespace Disco.Models.Services.Interop.DiscoServices
|
||||
|
||||
public List<StatisticIntPair> Stat_JobIdentifiers { get; set; }
|
||||
public List<StatisticJob> Stat_Jobs { get; set; }
|
||||
public List<StatisticInt> Stat_EnrollmentDiscovery { get; set; }
|
||||
|
||||
public class StatisticIntPair
|
||||
{
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
namespace Disco.Models.UI.Config.Enrolment
|
||||
using System;
|
||||
|
||||
namespace Disco.Models.UI.Config.Enrolment
|
||||
{
|
||||
public interface ConfigEnrolmentIndexModel : BaseUIModel
|
||||
{
|
||||
string MacSshUsername { get; set; }
|
||||
int PendingTimeoutMinutes { get; set; }
|
||||
Uri MacEnrolUrl { get; set; }
|
||||
bool HostingPluginInstalled { get; set; }
|
||||
bool IsVicSmartDeployment { get; set; }
|
||||
bool IsServicesEducationVicGovAuDomain { get; set; }
|
||||
string DnsSrvRecordName { get; set; }
|
||||
string DnsSrvRecordValue { get; set; }
|
||||
bool LegacyDiscoveryEnabled { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Disco.Data.Repository;
|
||||
using Disco.Models.ClientServices;
|
||||
using Disco.Models.Repository;
|
||||
using Disco.Models.Services.Devices;
|
||||
using Disco.Services.Authorization;
|
||||
using Disco.Services.Interop.ActiveDirectory;
|
||||
using Disco.Services.Users;
|
||||
@@ -18,6 +19,18 @@ namespace Disco.Services.Devices.Enrolment
|
||||
private static readonly string pendingIdentifierAlphabet = "23456789ABCDEFGHJKMNPQRSTWXYZ";
|
||||
private static readonly Random pendingIdentifierRng = new Random();
|
||||
private static readonly ConcurrentDictionary<string, EnrolResponse> pendingEnrolments = new ConcurrentDictionary<string, EnrolResponse>();
|
||||
private static readonly Dictionary<DeviceEnrolmentServerDiscoveryMethod, int> discoveryMethodStatistics = Enum.GetValues(typeof(DeviceEnrolmentServerDiscoveryMethod)).Cast<DeviceEnrolmentServerDiscoveryMethod>().ToDictionary(k => k, k => 0);
|
||||
|
||||
public static string GetDnsServiceLocationRecordName()
|
||||
=> $"_discoict._tcp.{ActiveDirectory.Context.PrimaryDomain.Name}";
|
||||
|
||||
public static void IncrementDiscoveryMethod(DeviceEnrolmentServerDiscoveryMethod method)
|
||||
{
|
||||
discoveryMethodStatistics[method]++;
|
||||
}
|
||||
|
||||
public static IEnumerable<KeyValuePair<DeviceEnrolmentServerDiscoveryMethod, int>> GetDiscoveryMethodStatistics()
|
||||
=> discoveryMethodStatistics.AsEnumerable();
|
||||
|
||||
private static void CleanupPendingEnrolments()
|
||||
{
|
||||
|
||||
@@ -497,6 +497,14 @@
|
||||
<Compile Include="Interop\DiscoServices\Upload\UploadOnlineClient.cs" />
|
||||
<Compile Include="Interop\DiscoServices\Upload\UploadOnlineService.cs" />
|
||||
<Compile Include="Interop\DiscoServices\Upload\UploadOnlineSyncTask.cs" />
|
||||
<Compile Include="Interop\DNS\ADnsRecord.cs" />
|
||||
<Compile Include="Interop\DNS\CnameDnsRecord.cs" />
|
||||
<Compile Include="Interop\DNS\DnsRecord.cs" />
|
||||
<Compile Include="Interop\DNS\DnsRecordType.cs" />
|
||||
<Compile Include="Interop\DNS\DnsService.cs" />
|
||||
<Compile Include="Interop\DNS\NativeDns.cs" />
|
||||
<Compile Include="Interop\DNS\SrvDnsRecord.cs" />
|
||||
<Compile Include="Interop\DNS\TxtDnsRecord.cs" />
|
||||
<Compile Include="Interop\IIS\PreserveIisBindingsTask.cs" />
|
||||
<Compile Include="Interop\MimeTypes.cs" />
|
||||
<Compile Include="Interop\VicEduDept\VicSmart.cs" />
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace Disco.Services.Interop.DNS
|
||||
{
|
||||
public class ADnsRecord : DnsRecord
|
||||
{
|
||||
public IPAddress Address { get; }
|
||||
|
||||
public ADnsRecord(string name, TimeSpan timeToLive, uint address)
|
||||
: base(name, DnsRecordType.A, timeToLive, UIntToIPAddress(address).ToString())
|
||||
{
|
||||
Address = UIntToIPAddress(address);
|
||||
}
|
||||
|
||||
private static IPAddress UIntToIPAddress(uint address)
|
||||
{
|
||||
byte[] bytes = BitConverter.GetBytes(address);
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(bytes);
|
||||
return new IPAddress(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace Disco.Services.Interop.DNS
|
||||
{
|
||||
public class CnameDnsRecord : DnsRecord
|
||||
{
|
||||
public CnameDnsRecord(string name, TimeSpan timeToLive, string canonicalName)
|
||||
: base(name, DnsRecordType.Cname, timeToLive, canonicalName)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace Disco.Services.Interop.DNS
|
||||
{
|
||||
public abstract class DnsRecord
|
||||
{
|
||||
public string Name { get; }
|
||||
public DnsRecordType Type { get; }
|
||||
public TimeSpan TimeToLive { get; }
|
||||
public string Content { get; }
|
||||
|
||||
protected DnsRecord(string name, DnsRecordType type, TimeSpan timeToLive, string content)
|
||||
{
|
||||
Name = name;
|
||||
Type = type;
|
||||
TimeToLive = timeToLive;
|
||||
Content = content;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Disco.Services.Interop.DNS
|
||||
{
|
||||
public enum DnsRecordType
|
||||
{
|
||||
A = 0x01,
|
||||
Cname = 0x05,
|
||||
Txt = 0x10,
|
||||
Srv = 0x21
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Disco.Services.Interop.DNS
|
||||
{
|
||||
public class DnsService
|
||||
{
|
||||
public DnsService()
|
||||
{
|
||||
}
|
||||
|
||||
public static List<T> Query<T>(string name, bool bypassCache = false) where T : DnsRecord
|
||||
{
|
||||
DnsRecordType recordType;
|
||||
if (typeof(T) == typeof(ADnsRecord))
|
||||
recordType = DnsRecordType.A;
|
||||
else if (typeof(T) == typeof(CnameDnsRecord))
|
||||
recordType = DnsRecordType.Cname;
|
||||
else if (typeof(T) == typeof(TxtDnsRecord))
|
||||
recordType = DnsRecordType.Txt;
|
||||
else if (typeof(T) == typeof(SrvDnsRecord))
|
||||
recordType = DnsRecordType.Srv;
|
||||
else
|
||||
throw new NotSupportedException($"Unsupported DNS record type: {typeof(T).Name}");
|
||||
var records = NativeDns.QueryRecords(recordType, name, bypassCache);
|
||||
return records.ConvertAll(r => (T)r);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace Disco.Services.Interop.DNS
|
||||
{
|
||||
internal static class NativeDns
|
||||
{
|
||||
|
||||
[DllImport("dnsapi", EntryPoint = "DnsQuery_W", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)]
|
||||
private static extern int DnsQuery([MarshalAs(UnmanagedType.VBByRefStr)] ref string pszName, NativeDnsQueryTypes wType, NativeDnsQueryOptions options, int aipServers, ref IntPtr ppQueryResults, int pReserved);
|
||||
|
||||
[DllImport("dnsapi", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
private static extern void DnsRecordListFree(IntPtr pRecordList, int FreeType);
|
||||
private const int DNS_ERROR_RCODE_NAME_ERROR = 0x232B;
|
||||
private const int DNS_ERROR_BAD_PACKET = 0x251E;
|
||||
|
||||
public static List<DnsRecord> QueryRecords(DnsRecordType type, string name, bool bypassCache)
|
||||
{
|
||||
NativeDnsQueryTypes queryType;
|
||||
Func<IntPtr, Tuple<DnsRecord, IntPtr>> marshaller;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case DnsRecordType.A:
|
||||
queryType = NativeDnsQueryTypes.DNS_TYPE_A;
|
||||
marshaller = MarshalARecord;
|
||||
break;
|
||||
case DnsRecordType.Cname:
|
||||
queryType = NativeDnsQueryTypes.DNS_TYPE_CNAME;
|
||||
marshaller = MarshalCnameRecord;
|
||||
break;
|
||||
case DnsRecordType.Txt:
|
||||
queryType = NativeDnsQueryTypes.DNS_TYPE_TEXT;
|
||||
marshaller = MarshalTxtRecord;
|
||||
break;
|
||||
case DnsRecordType.Srv:
|
||||
queryType = NativeDnsQueryTypes.DNS_TYPE_SRV;
|
||||
marshaller = MarshalSrvRecord;
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Unsupported DNS record type: {type}");
|
||||
}
|
||||
|
||||
IntPtr rrPointers = IntPtr.Zero;
|
||||
var records = new List<DnsRecord>();
|
||||
var retry = 5;
|
||||
retry:
|
||||
try
|
||||
{
|
||||
int queryResult = DnsQuery(ref name, queryType, bypassCache ? NativeDnsQueryOptions.DNS_QUERY_BYPASS_CACHE : NativeDnsQueryOptions.DNS_QUERY_STANDARD, 0, ref rrPointers, 0);
|
||||
if (queryResult != 0)
|
||||
{
|
||||
if (queryResult == DNS_ERROR_RCODE_NAME_ERROR)
|
||||
return records;
|
||||
else if (queryResult == DNS_ERROR_BAD_PACKET && retry > 0)
|
||||
{
|
||||
// Sometimes a BAD_PACKET error is returned, retry a few times
|
||||
Thread.Sleep(100);
|
||||
retry--;
|
||||
goto retry;
|
||||
}
|
||||
else
|
||||
throw new Win32Exception(queryResult);
|
||||
}
|
||||
for (var rrPointer = rrPointers; !rrPointer.Equals(IntPtr.Zero);)
|
||||
{
|
||||
var (record, rrPointerNext) = marshaller(rrPointer);
|
||||
records.Add(record);
|
||||
rrPointer = rrPointerNext;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rrPointers != IntPtr.Zero)
|
||||
DnsRecordListFree(rrPointers, 0);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
private static Tuple<DnsRecord, IntPtr> MarshalARecord(IntPtr pointer)
|
||||
{
|
||||
var native = Marshal.PtrToStructure<NativeDnsAData>(pointer);
|
||||
var record = new ADnsRecord(native.pName, TimeSpan.FromSeconds(native.dwTtl), native.IpAddress);
|
||||
return Tuple.Create((DnsRecord)record, native.pNext);
|
||||
}
|
||||
|
||||
private static Tuple<DnsRecord, IntPtr> MarshalCnameRecord(IntPtr pointer)
|
||||
{
|
||||
var native = Marshal.PtrToStructure<NativeDnsPtrData>(pointer);
|
||||
var record = new CnameDnsRecord(native.pName, TimeSpan.FromSeconds(native.dwTtl), native.pNameHost);
|
||||
return Tuple.Create((DnsRecord)record, native.pNext);
|
||||
}
|
||||
|
||||
private static Tuple<DnsRecord, IntPtr> MarshalTxtRecord(IntPtr pointer)
|
||||
{
|
||||
var native = Marshal.PtrToStructure<NativeDnsTxtData>(pointer);
|
||||
var record = new TxtDnsRecord(native.pName, TimeSpan.FromSeconds(native.dwTtl), native.pStringArray);
|
||||
return Tuple.Create((DnsRecord)record, native.pNext);
|
||||
}
|
||||
|
||||
private static Tuple<DnsRecord, IntPtr> MarshalSrvRecord(IntPtr pointer)
|
||||
{
|
||||
var native = Marshal.PtrToStructure<NativeDnsSrvData>(pointer);
|
||||
var record = new SrvDnsRecord(native.pName, TimeSpan.FromSeconds(native.dwTtl), native.pNameTarget, native.wPriority, native.wWeight, native.wPort);
|
||||
return Tuple.Create((DnsRecord)record, native.pNext);
|
||||
}
|
||||
|
||||
private enum NativeDnsQueryOptions
|
||||
{
|
||||
DNS_QUERY_ACCEPT_TRUNCATED_RESPONSE = 1,
|
||||
DNS_QUERY_BYPASS_CACHE = 8,
|
||||
DNS_QUERY_DONT_RESET_TTL_VALUES = 0x100000,
|
||||
DNS_QUERY_NO_HOSTS_FILE = 0x40,
|
||||
DNS_QUERY_NO_LOCAL_NAME = 0x20,
|
||||
DNS_QUERY_NO_NETBT = 0x80,
|
||||
DNS_QUERY_NO_RECURSION = 4,
|
||||
DNS_QUERY_NO_WIRE_QUERY = 0x10,
|
||||
DNS_QUERY_RESERVED = -16777216,
|
||||
DNS_QUERY_RETURN_MESSAGE = 0x200,
|
||||
DNS_QUERY_STANDARD = 0,
|
||||
DNS_QUERY_TREAT_AS_FQDN = 0x1000,
|
||||
DNS_QUERY_USE_TCP_ONLY = 2,
|
||||
DNS_QUERY_WIRE_ONLY = 0x100
|
||||
}
|
||||
|
||||
private enum NativeDnsQueryTypes
|
||||
{
|
||||
DNS_TYPE_A = 0x0001,
|
||||
DNS_TYPE_CNAME = 0x0005,
|
||||
DNS_TYPE_TEXT = 0x0010,
|
||||
DNS_TYPE_SRV = 0x0021
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct NativeDnsSrvData
|
||||
{
|
||||
public IntPtr pNext;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string pName;
|
||||
public ushort wType;
|
||||
public ushort wDataLength;
|
||||
public int flags;
|
||||
public int dwTtl;
|
||||
public int dwReserved;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string pNameTarget;
|
||||
public ushort wPriority;
|
||||
public ushort wWeight;
|
||||
public ushort wPort;
|
||||
public ushort Pad;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct NativeDnsTxtData
|
||||
{
|
||||
public IntPtr pNext;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string pName;
|
||||
public ushort wType;
|
||||
public ushort wDataLength;
|
||||
public int flags;
|
||||
public int dwTtl;
|
||||
public int dwReserved;
|
||||
public uint dwStringLength;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string pStringArray;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct NativeDnsPtrData
|
||||
{
|
||||
public IntPtr pNext;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string pName;
|
||||
public ushort wType;
|
||||
public ushort wDataLength;
|
||||
public int flags;
|
||||
public int dwTtl;
|
||||
public int dwReserved;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string pNameHost;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct NativeDnsAData
|
||||
{
|
||||
public IntPtr pNext;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string pName;
|
||||
public ushort wType;
|
||||
public ushort wDataLength;
|
||||
public int flags;
|
||||
public int dwTtl;
|
||||
public int dwReserved;
|
||||
public uint IpAddress;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
|
||||
namespace Disco.Services.Interop.DNS
|
||||
{
|
||||
public class SrvDnsRecord : DnsRecord
|
||||
{
|
||||
public string Target { get; }
|
||||
public ushort Priority { get; }
|
||||
public ushort Weight { get; }
|
||||
public ushort Port { get; }
|
||||
|
||||
public SrvDnsRecord(string name, TimeSpan timeToLive, string target, ushort priority, ushort weight, ushort port)
|
||||
: base(name, DnsRecordType.Srv, timeToLive, $"{priority} {weight} {port} {target}")
|
||||
{
|
||||
Target = target;
|
||||
Priority = priority;
|
||||
Weight = weight;
|
||||
Port = port;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace Disco.Services.Interop.DNS
|
||||
{
|
||||
public class TxtDnsRecord : DnsRecord
|
||||
{
|
||||
public TxtDnsRecord(string name, TimeSpan timeToLive, string text)
|
||||
: base(name, DnsRecordType.Txt, timeToLive, text)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Disco.Data.Repository;
|
||||
using Disco.Models.Repository;
|
||||
using Disco.Models.Services.Interop.DiscoServices;
|
||||
using Disco.Services.Devices.Enrolment;
|
||||
using Disco.Services.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
@@ -221,6 +222,10 @@ namespace Disco.Services.Interop.DiscoServices
|
||||
RepairerLogged = j.JobType == JobType.JobTypeIds.HWar ? j.WarrantyRepairerLoggedDate : j.RepairerLoggedDate,
|
||||
RepairerCompleted = j.JobType == JobType.JobTypeIds.HWar ? j.WarrantyRepairerCompletedDate : j.RepairerCompletedDate
|
||||
}).ToList();
|
||||
|
||||
m.Stat_EnrollmentDiscovery = WindowsDeviceEnrolment.GetDiscoveryMethodStatistics()
|
||||
.Where(s => s.Value != 0)
|
||||
.Select(s => new StatisticInt() { Key = s.Key.ToString(), Value = s.Value }).ToList();
|
||||
}
|
||||
|
||||
m.InstalledPlugins = Plugins.Plugins.GetPlugins().Select(manifest => new StatisticString() { Key = manifest.Id, Value = manifest.VersionFormatted }).ToList();
|
||||
|
||||
@@ -1,13 +1,51 @@
|
||||
using Disco.Services.Interop.DiscoServices;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Disco.Services.Interop.VicEduDept
|
||||
{
|
||||
public class VicSmart
|
||||
{
|
||||
public static bool IsVicSmartDeployment()
|
||||
{
|
||||
var nics = NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(ni => ni.OperationalStatus == OperationalStatus.Up)
|
||||
.ToList();
|
||||
|
||||
bool found10Net = false;
|
||||
foreach (var nic in nics)
|
||||
{
|
||||
if (nic.Supports(NetworkInterfaceComponent.IPv4))
|
||||
{
|
||||
var ipProps = nic.GetIPProperties();
|
||||
var ipv4Props = ipProps.GetIPv4Properties();
|
||||
if (ipv4Props.IsAutomaticPrivateAddressingActive)
|
||||
continue;
|
||||
|
||||
found10Net = ipProps.UnicastAddresses
|
||||
.Where(ua =>
|
||||
ua.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork &&
|
||||
ua.Address.GetAddressBytes()[0] == 10)
|
||||
.Any();
|
||||
if (found10Net)
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found10Net)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var entry = Dns.GetHostEntry("broadband.doe.wan");
|
||||
return entry.AddressList.Length > 0;
|
||||
}
|
||||
catch (Exception)
|
||||
{ return false; } // Fail on error
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queries DoE VicSmart Service to detect the current site.
|
||||
|
||||
@@ -88,5 +88,21 @@ namespace Disco.Web.Areas.API.Controllers
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[DiscoAuthorize(Claims.Config.Enrolment.Configure)]
|
||||
[HttpPost, ValidateAntiForgeryToken]
|
||||
public virtual ActionResult LegacyDiscovery(bool enabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
Database.DiscoConfiguration.Devices.EnrollmentLegacyDiscoveryDisabled = !enabled;
|
||||
Database.SaveChanges();
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
using Disco.Models.UI.Config.Enrolment;
|
||||
using Disco.Services.Authorization;
|
||||
using Disco.Services.Devices.Enrolment;
|
||||
using Disco.Services.Interop.ActiveDirectory;
|
||||
using Disco.Services.Interop.DNS;
|
||||
using Disco.Services.Interop.VicEduDept;
|
||||
using Disco.Services.Plugins;
|
||||
using Disco.Services.Plugins.Features.UIExtension;
|
||||
using Disco.Services.Web;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Web.Mvc;
|
||||
|
||||
@@ -12,10 +18,30 @@ namespace Disco.Web.Areas.Config.Controllers
|
||||
[DiscoAuthorize(Claims.Config.Enrolment.Show)]
|
||||
public virtual ActionResult Index()
|
||||
{
|
||||
var serverUrl = Request.Url;
|
||||
if ((serverUrl.HostNameType == UriHostNameType.Dns && serverUrl.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase)) ||
|
||||
serverUrl.HostNameType == UriHostNameType.IPv4 || serverUrl.HostNameType == UriHostNameType.IPv6)
|
||||
{
|
||||
serverUrl = new UriBuilder(serverUrl)
|
||||
{
|
||||
Host = Environment.MachineName
|
||||
}.Uri;
|
||||
}
|
||||
|
||||
var srvRecord = DnsService.Query<SrvDnsRecord>(WindowsDeviceEnrolment.GetDnsServiceLocationRecordName(), true).FirstOrDefault();
|
||||
var srvValue = srvRecord == null ? null : (srvRecord.Port == 443 ? srvRecord.Target : $"{srvRecord.Target}:{srvRecord.Port}");
|
||||
|
||||
var m = new Models.Enrolment.IndexModel()
|
||||
{
|
||||
MacSshUsername = Database.DiscoConfiguration.Bootstrapper.MacSshUsername,
|
||||
PendingTimeoutMinutes = (int)Database.DiscoConfiguration.Bootstrapper.PendingTimeout.TotalMinutes,
|
||||
MacEnrolUrl = new Uri(serverUrl, Url.Action(MVC.Services.Client.Unauthenticated("MacSecureEnrol"))),
|
||||
HostingPluginInstalled = Plugins.PluginInstalled("Hosting"),
|
||||
IsServicesEducationVicGovAuDomain = ActiveDirectory.Context.PrimaryDomain.Name.Equals("services.education.vic.gov.au", StringComparison.OrdinalIgnoreCase),
|
||||
IsVicSmartDeployment = VicSmart.IsVicSmartDeployment(),
|
||||
DnsSrvRecordName = WindowsDeviceEnrolment.GetDnsServiceLocationRecordName(),
|
||||
DnsSrvRecordValue = srvValue,
|
||||
LegacyDiscoveryEnabled = !Database.DiscoConfiguration.Devices.EnrollmentLegacyDiscoveryDisabled,
|
||||
};
|
||||
|
||||
// UI Extensions
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Disco.Models.UI.Config.Enrolment;
|
||||
using System;
|
||||
|
||||
namespace Disco.Web.Areas.Config.Models.Enrolment
|
||||
{
|
||||
@@ -6,5 +7,12 @@ namespace Disco.Web.Areas.Config.Models.Enrolment
|
||||
{
|
||||
public string MacSshUsername { get; set; }
|
||||
public int PendingTimeoutMinutes { get; set; }
|
||||
public Uri MacEnrolUrl { get; set; }
|
||||
public bool HostingPluginInstalled { get; set; }
|
||||
public bool IsVicSmartDeployment { get; set; }
|
||||
public bool IsServicesEducationVicGovAuDomain { get; set; }
|
||||
public string DnsSrvRecordName { get; set; }
|
||||
public string DnsSrvRecordValue { get; set; }
|
||||
public bool LegacyDiscoveryEnabled { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -121,7 +121,7 @@
|
||||
able to connect to the requesting Apple Mac client via <a target="_blank" href="http://en.wikipedia.org/wiki/Secure_Shell">SSH</a>. Enter/Script the following command:
|
||||
</span>
|
||||
<div class="code">
|
||||
curl <a target="_blank" href="http://disco:9292/Services/Client/Unauthenticated/MacSecureEnrol">http://disco:9292/Services/Client/Unauthenticated/MacSecureEnrol</a>
|
||||
curl <a target="_blank" href="@Model.MacEnrolUrl">@Model.MacEnrolUrl</a>
|
||||
</div>
|
||||
<span class="smallText">This url will return a <a target="_blank" href="http://json.org/">JSON</a> response containing basic information about the enrolment.</span><br />
|
||||
<span class="smallMessage">
|
||||
@@ -133,6 +133,167 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="form" style="width: 530px; margin-top: 15px">
|
||||
<h2>Bootstrapper Server Discovery</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<div>
|
||||
The Disco ICT
|
||||
@if (Authorization.Has(Claims.Config.Enrolment.DownloadBootstrapper))
|
||||
{
|
||||
@Html.ActionLink("Bootstrapper", MVC.Services.Client.Bootstrapper())
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>Bootstrapper</text>
|
||||
}
|
||||
is used to enrol devices. It is strongly recommended that HTTPS be used for all communication.
|
||||
the
|
||||
The @Html.ActionLink("Hosting", Model.HostingPluginInstalled ? MVC.Config.Plugins.Configure("Hosting") : MVC.Config.Plugins.Install())
|
||||
plugin can be used to automate deployment of HTTPS certificates.
|
||||
</div>
|
||||
<div>
|
||||
The Bootstrapper discovers the server using the first successful method (in order):
|
||||
</div>
|
||||
<ol>
|
||||
<li>
|
||||
<h5>Manually Specified</h5>
|
||||
<div>
|
||||
The server url can be specified at the command line. The url must use HTTPS. For example:
|
||||
</div>
|
||||
<div class="code">Disco.ClientBootstrapper.exe https://@Request.Url.Authority</div>
|
||||
</li>
|
||||
<li>
|
||||
<h5>DNS Service Location (SRV) Record</h5>
|
||||
Expected Record Name: <strong><code>@Model.DnsSrvRecordName</code></strong>
|
||||
@if (Model.IsServicesEducationVicGovAuDomain)
|
||||
{
|
||||
<div class="smallText">
|
||||
This mechanism is not supported in the shared education.vic.gov.au domain and can be ignored.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Model.DnsSrvRecordValue == null)
|
||||
{
|
||||
<div class="info-box">
|
||||
<span class="error">
|
||||
No Service Location (SRV) record found.
|
||||
</span>
|
||||
@if (Request.IsSecureConnection)
|
||||
{
|
||||
<span>
|
||||
Please create a DNS Service Location (SRV) record:
|
||||
</span>
|
||||
<table class="none">
|
||||
<tr>
|
||||
<th>Service:</th>
|
||||
<td><code>_discoict</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Protocol:</th>
|
||||
<td><code>_tcp</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Priority:</th>
|
||||
<td><code>0</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Weight:</th>
|
||||
<td><code>0</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Port:</th>
|
||||
<td><code>@Request.Url.Port</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Host offering this service:</th>
|
||||
<td><code>@Request.Url.Host</code></td>
|
||||
</tr>
|
||||
</table>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div>
|
||||
Please configure and connect with HTTPS.
|
||||
<span>
|
||||
You can enable HTTPS automation using the
|
||||
@Html.ActionLink("Hosting", Model.HostingPluginInstalled ? MVC.Config.Plugins.Configure("Hosting") : MVC.Config.Plugins.Install())
|
||||
plugin.
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div>
|
||||
Value: <strong><code>https://@Model.DnsSrvRecordValue</code></strong>
|
||||
@if (Request.IsSecureConnection && !string.Equals(Model.DnsSrvRecordValue, Request.Url.Authority, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
<div class="info-box error">
|
||||
<i class="fa fa-exclamation"></i> The Service Location (SRV) record does not match the way you are currently accessing the server: <code>@Request.Url.Authority</code>.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</li>
|
||||
@if (Model.IsVicSmartDeployment)
|
||||
{
|
||||
<li>
|
||||
<h5>Victorian Government Schools VicSmart Discovery</h5>
|
||||
If the Bootstrapper detects it is running inside the VicSmart network, it will query Online Services for the Disco ICT server address based on the subnets assigned to each school.
|
||||
This is configured in the @Html.ActionLink("Hosting", Model.HostingPluginInstalled ? MVC.Config.Plugins.Configure("Hosting") : MVC.Config.Plugins.Install())
|
||||
plugin.
|
||||
</li>
|
||||
}
|
||||
<li>
|
||||
<h5>Legacy Discovery</h5>
|
||||
<div>
|
||||
The Bootstrapper will attempt to send an ICMP ping to "<code>disco</code>". If the ping is successful, it will attempt to connect to <code>http://disco:9292/</code>.
|
||||
</div>
|
||||
<div>
|
||||
@if (canConfig)
|
||||
{
|
||||
<input id="Enrolment_LegacyDiscovery" type="checkbox" @(Model.LegacyDiscoveryEnabled ? "checked" : null) />
|
||||
<script type="text/javascript">
|
||||
$(function () {
|
||||
document.DiscoFunctions.PropertyChangeHelper(
|
||||
$('#Enrolment_LegacyDiscovery'),
|
||||
null,
|
||||
'@Url.Action(MVC.API.Enrolment.LegacyDiscovery())',
|
||||
'enabled'
|
||||
);
|
||||
});
|
||||
</script>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input id="Enrolment_LegacyDiscovery" type="checkbox" @(Model.LegacyDiscoveryEnabled ? "checked" : null) disabled="disabled" />
|
||||
}
|
||||
<label for="Enrolment_LegacyDiscovery">
|
||||
Legacy Discovery Enabled
|
||||
</label>
|
||||
@AjaxHelpers.AjaxLoader()
|
||||
</div>
|
||||
@if ((Model.IsServicesEducationVicGovAuDomain || Model.DnsSrvRecordValue != null) && Model.LegacyDiscoveryEnabled)
|
||||
{
|
||||
<div class="info-box error">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
It is not recommended to have Legacy Discovery enabled. Please use the latest Bootstrapper and disable this option.
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
This method is not secure and is only provided for backwards compatibility. In time this method will be removed.
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@if (canShowStatus && Authorization.Has(Claims.Config.Logging.Show))
|
||||
{
|
||||
<h2>Live Enrolment Logging</h2>
|
||||
|
||||
@@ -451,10 +451,26 @@ WriteLiteral(">\r\n curl <a");
|
||||
|
||||
WriteLiteral(" target=\"_blank\"");
|
||||
|
||||
WriteLiteral(" href=\"http://disco:9292/Services/Client/Unauthenticated/MacSecureEnrol\"");
|
||||
WriteAttribute("href", Tuple.Create(" href=\"", 4881), Tuple.Create("\"", 4906)
|
||||
|
||||
WriteLiteral(">http://disco:9292/Services/Client/Unauthenticated/MacSecureEnrol</a>\r\n " +
|
||||
" </div>\r\n <span");
|
||||
#line 124 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
, Tuple.Create(Tuple.Create("", 4888), Tuple.Create<System.Object, System.Int32>(Model.MacEnrolUrl
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
, 4888), false)
|
||||
);
|
||||
|
||||
WriteLiteral(">");
|
||||
|
||||
|
||||
#line 124 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
Write(Model.MacEnrolUrl);
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral("</a>\r\n </div>\r\n <span");
|
||||
|
||||
WriteLiteral(" class=\"smallText\"");
|
||||
|
||||
@@ -486,10 +502,521 @@ WriteLiteral(" class=\"code\"");
|
||||
|
||||
WriteLiteral("><script></span>\r\n tag embedded on the organisation\'s in" +
|
||||
"tranet.\r\n </span>\r\n </td>\r\n </tr>\r\n </table>" +
|
||||
"\r\n</div>\r\n");
|
||||
"\r\n</div>\r\n<div");
|
||||
|
||||
WriteLiteral(" class=\"form\"");
|
||||
|
||||
WriteLiteral(" style=\"width: 530px; margin-top: 15px\"");
|
||||
|
||||
WriteLiteral(">\r\n <h2>Bootstrapper Server Discovery</h2>\r\n <table>\r\n <tr>\r\n " +
|
||||
" <td>\r\n <div>\r\n The Disco ICT\r\n");
|
||||
|
||||
|
||||
#line 136 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
#line 143 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 143 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
if (Authorization.Has(Claims.Config.Enrolment.DownloadBootstrapper))
|
||||
{
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 145 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
Write(Html.ActionLink("Bootstrapper", MVC.Services.Client.Bootstrapper()));
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 145 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(" ");
|
||||
|
||||
WriteLiteral("Bootstrapper");
|
||||
|
||||
WriteLiteral("\r\n");
|
||||
|
||||
|
||||
#line 150 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
}
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(" is used to enrol devices. It is strongly recommended that HTT" +
|
||||
"PS be used for all communication.\r\n the\r\n " +
|
||||
"The ");
|
||||
|
||||
|
||||
#line 153 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
Write(Html.ActionLink("Hosting", Model.HostingPluginInstalled ? MVC.Config.Plugins.Configure("Hosting") : MVC.Config.Plugins.Install()));
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(@"
|
||||
plugin can be used to automate deployment of HTTPS certificates.
|
||||
</div>
|
||||
<div>
|
||||
The Bootstrapper discovers the server using the first successful method (in order):
|
||||
</div>
|
||||
<ol>
|
||||
<li>
|
||||
<h5>Manually Specified</h5>
|
||||
<div>
|
||||
The server url can be specified at the command line. The url must use HTTPS. For example:
|
||||
</div>
|
||||
<div");
|
||||
|
||||
WriteLiteral(" class=\"code\"");
|
||||
|
||||
WriteLiteral(">Disco.ClientBootstrapper.exe https://");
|
||||
|
||||
|
||||
#line 165 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
Write(Request.Url.Authority);
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral("</div>\r\n </li>\r\n <li>\r\n " +
|
||||
" <h5>DNS Service Location (SRV) Record</h5>\r\n Expected" +
|
||||
" Record Name: <strong><code>");
|
||||
|
||||
|
||||
#line 169 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
Write(Model.DnsSrvRecordName);
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral("</code></strong>\r\n");
|
||||
|
||||
|
||||
#line 170 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 170 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
if (Model.IsServicesEducationVicGovAuDomain)
|
||||
{
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(" <div");
|
||||
|
||||
WriteLiteral(" class=\"smallText\"");
|
||||
|
||||
WriteLiteral(">\r\n This mechanism is not supported in the shared " +
|
||||
"education.vic.gov.au domain and can be ignored.\r\n </d" +
|
||||
"iv>\r\n");
|
||||
|
||||
|
||||
#line 175 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Model.DnsSrvRecordValue == null)
|
||||
{
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(" <div");
|
||||
|
||||
WriteLiteral(" class=\"info-box\"");
|
||||
|
||||
WriteLiteral(">\r\n <span");
|
||||
|
||||
WriteLiteral(" class=\"error\"");
|
||||
|
||||
WriteLiteral(">\r\n No Service Location (SRV) record found" +
|
||||
".\r\n </span>\r\n");
|
||||
|
||||
|
||||
#line 184 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 184 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
if (Request.IsSecureConnection)
|
||||
{
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(" <span>\r\n " +
|
||||
" Please create a DNS Service Location (SRV) record:\r\n " +
|
||||
" </span>\r\n");
|
||||
|
||||
WriteLiteral(" <table");
|
||||
|
||||
WriteLiteral(" class=\"none\"");
|
||||
|
||||
WriteLiteral(@">
|
||||
<tr>
|
||||
<th>Service:</th>
|
||||
<td><code>_discoict</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Protocol:</th>
|
||||
<td><code>_tcp</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Priority:</th>
|
||||
<td><code>0</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Weight:</th>
|
||||
<td><code>0</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Port:</th>
|
||||
<td><code>");
|
||||
|
||||
|
||||
#line 208 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
Write(Request.Url.Port);
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(@"</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Host offering this service:</th>
|
||||
<td><code>");
|
||||
|
||||
|
||||
#line 212 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
Write(Request.Url.Host);
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral("</code></td>\r\n </tr>\r\n " +
|
||||
" </table>\r\n");
|
||||
|
||||
|
||||
#line 215 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(@" <div>
|
||||
Please configure and connect with HTTPS.
|
||||
<span>
|
||||
You can enable HTTPS automation using the
|
||||
");
|
||||
|
||||
WriteLiteral(" ");
|
||||
|
||||
|
||||
#line 222 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
Write(Html.ActionLink("Hosting", Model.HostingPluginInstalled ? MVC.Config.Plugins.Configure("Hosting") : MVC.Config.Plugins.Install()));
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral("\r\n plugin.\r\n " +
|
||||
" </span>\r\n </div>\r\n");
|
||||
|
||||
|
||||
#line 226 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
}
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(" </div>\r\n");
|
||||
|
||||
|
||||
#line 228 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(" <div>\r\n Value:" +
|
||||
" <strong><code>https://");
|
||||
|
||||
|
||||
#line 232 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
Write(Model.DnsSrvRecordValue);
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral("</code></strong>\r\n");
|
||||
|
||||
|
||||
#line 233 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 233 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
if (Request.IsSecureConnection && !string.Equals(Model.DnsSrvRecordValue, Request.Url.Authority, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(" <div");
|
||||
|
||||
WriteLiteral(" class=\"info-box error\"");
|
||||
|
||||
WriteLiteral(">\r\n <i");
|
||||
|
||||
WriteLiteral(" class=\"fa fa-exclamation\"");
|
||||
|
||||
WriteLiteral("></i> The Service Location (SRV) record does not match the way you are currently " +
|
||||
"accessing the server: <code>");
|
||||
|
||||
|
||||
#line 236 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
Write(Request.Url.Authority);
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral("</code>.\r\n </div>\r\n");
|
||||
|
||||
|
||||
#line 238 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
}
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(" </div>\r\n");
|
||||
|
||||
|
||||
#line 240 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(" </li>\r\n");
|
||||
|
||||
|
||||
#line 243 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 243 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
if (Model.IsVicSmartDeployment)
|
||||
{
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(@" <li>
|
||||
<h5>Victorian Government Schools VicSmart Discovery</h5>
|
||||
If the Bootstrapper detects it is running inside the VicSmart network, it will query Online Services for the Disco ICT server address based on the subnets assigned to each school.
|
||||
This is configured in the ");
|
||||
|
||||
|
||||
#line 248 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
Write(Html.ActionLink("Hosting", Model.HostingPluginInstalled ? MVC.Config.Plugins.Configure("Hosting") : MVC.Config.Plugins.Install()));
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral("\r\n plugin.\r\n </li>\r\n");
|
||||
|
||||
|
||||
#line 251 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
}
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(@" <li>
|
||||
<h5>Legacy Discovery</h5>
|
||||
<div>
|
||||
The Bootstrapper will attempt to send an ICMP ping to "<code>disco</code>". If the ping is successful, it will attempt to connect to <code>http://disco:9292/</code>.
|
||||
</div>
|
||||
<div>
|
||||
");
|
||||
|
||||
|
||||
#line 258 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 258 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
if (canConfig)
|
||||
{
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(" <input");
|
||||
|
||||
WriteLiteral(" id=\"Enrolment_LegacyDiscovery\"");
|
||||
|
||||
WriteLiteral(" type=\"checkbox\"");
|
||||
|
||||
WriteLiteral(" ");
|
||||
|
||||
|
||||
#line 260 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
Write(Model.LegacyDiscoveryEnabled ? "checked" : null);
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(" />\r\n");
|
||||
|
||||
WriteLiteral(" <script");
|
||||
|
||||
WriteLiteral(" type=\"text/javascript\"");
|
||||
|
||||
WriteLiteral(@">
|
||||
$(function () {
|
||||
document.DiscoFunctions.PropertyChangeHelper(
|
||||
$('#Enrolment_LegacyDiscovery'),
|
||||
null,
|
||||
'");
|
||||
|
||||
|
||||
#line 266 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
Write(Url.Action(MVC.API.Enrolment.LegacyDiscovery()));
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral("\',\r\n \'enabled\'\r\n " +
|
||||
" );\r\n });\r\n " +
|
||||
" </script>\r\n");
|
||||
|
||||
|
||||
#line 271 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(" <input");
|
||||
|
||||
WriteLiteral(" id=\"Enrolment_LegacyDiscovery\"");
|
||||
|
||||
WriteLiteral(" type=\"checkbox\"");
|
||||
|
||||
WriteLiteral(" ");
|
||||
|
||||
|
||||
#line 274 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
Write(Model.LegacyDiscoveryEnabled ? "checked" : null);
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(" disabled=\"disabled\" />\r\n");
|
||||
|
||||
|
||||
#line 275 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
}
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(" <label");
|
||||
|
||||
WriteLiteral(" for=\"Enrolment_LegacyDiscovery\"");
|
||||
|
||||
WriteLiteral(">\r\n Legacy Discovery Enabled\r\n " +
|
||||
" </label>\r\n");
|
||||
|
||||
WriteLiteral(" ");
|
||||
|
||||
|
||||
#line 279 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
Write(AjaxHelpers.AjaxLoader());
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral("\r\n </div>\r\n");
|
||||
|
||||
|
||||
#line 281 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 281 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
if ((Model.IsServicesEducationVicGovAuDomain || Model.DnsSrvRecordValue != null) && Model.LegacyDiscoveryEnabled)
|
||||
{
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(" <div");
|
||||
|
||||
WriteLiteral(" class=\"info-box error\"");
|
||||
|
||||
WriteLiteral(">\r\n <i");
|
||||
|
||||
WriteLiteral(" class=\"fa fa-exclamation-triangle\"");
|
||||
|
||||
WriteLiteral("></i>\r\n It is not recommended to have Legacy Disco" +
|
||||
"very enabled. Please use the latest Bootstrapper and disable this option.\r\n " +
|
||||
" </div>\r\n");
|
||||
|
||||
|
||||
#line 287 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
}
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(@" <div>
|
||||
This method is not secure and is only provided for backwards compatibility. In time this method will be removed.
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
");
|
||||
|
||||
|
||||
#line 297 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
if (canShowStatus && Authorization.Has(Claims.Config.Logging.Show))
|
||||
{
|
||||
|
||||
@@ -499,13 +1026,13 @@ WriteLiteral("><script></span>\r\n tag embedded on the
|
||||
WriteLiteral(" <h2>Live Enrolment Logging</h2>\r\n");
|
||||
|
||||
|
||||
#line 139 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
#line 300 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 139 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
#line 300 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
Write(Html.Partial(MVC.Config.Shared.Views.LogEvents, new Disco.Web.Areas.Config.Models.Shared.LogEventsModel()
|
||||
{
|
||||
IsLive = true,
|
||||
@@ -519,7 +1046,7 @@ Write(Html.Partial(MVC.Config.Shared.Views.LogEvents, new Disco.Web.Areas.Config
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 146 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
#line 307 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
|
||||
}
|
||||
|
||||
@@ -533,13 +1060,13 @@ WriteLiteral(" class=\"actionBar\"");
|
||||
WriteLiteral(">\r\n");
|
||||
|
||||
|
||||
#line 149 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
#line 310 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 149 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
#line 310 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
if (Authorization.Has(Claims.Config.Enrolment.DownloadBootstrapper))
|
||||
{
|
||||
|
||||
@@ -547,14 +1074,14 @@ WriteLiteral(">\r\n");
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 151 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
#line 312 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
Write(Html.ActionLinkButton("Download Bootstrapper", MVC.Services.Client.Bootstrapper()));
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 151 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
#line 312 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
|
||||
}
|
||||
|
||||
@@ -564,7 +1091,7 @@ WriteLiteral(">\r\n");
|
||||
WriteLiteral(" ");
|
||||
|
||||
|
||||
#line 153 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
#line 314 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
if (canShowStatus)
|
||||
{
|
||||
|
||||
@@ -572,14 +1099,14 @@ WriteLiteral(" ");
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 155 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
#line 316 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
Write(Html.ActionLinkButton("Enrolment Status", MVC.Config.Enrolment.Status()));
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 155 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
#line 316 "..\..\Areas\Config\Views\Enrolment\Index.cshtml"
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Disco.Data.Repository;
|
||||
using Disco.Models.ClientServices;
|
||||
using Disco.Models.Services.Devices;
|
||||
using Disco.Services;
|
||||
using Disco.Services.Authorization;
|
||||
using Disco.Services.Devices.Enrolment;
|
||||
@@ -22,11 +23,21 @@ namespace Disco.Web.Areas.Services.Controllers
|
||||
|
||||
public virtual ActionResult PreparationClient()
|
||||
{
|
||||
var discoveryMethodHeader = Request.Headers["X-DiscoICT-Discovery"];
|
||||
if (!string.IsNullOrEmpty(discoveryMethodHeader) && Enum.TryParse<DeviceEnrolmentServerDiscoveryMethod>(discoveryMethodHeader, out var discoveryMethod))
|
||||
WindowsDeviceEnrolment.IncrementDiscoveryMethod(discoveryMethod);
|
||||
|
||||
if (!CheckLegacyEnrollmentDiscovery())
|
||||
return BadRequest("Enrollment Legacy Discovery is disabled. Please use secure connection (HTTPS) for device enrollment.");
|
||||
|
||||
return File(Links.ClientBin.PreparationClient_zip, "application/x-msdownload", "PreparationClient.zip");
|
||||
}
|
||||
|
||||
public virtual ActionResult Unauthenticated(string feature)
|
||||
{
|
||||
if (!CheckLegacyEnrollmentDiscovery())
|
||||
return BadRequest("Enrollment Legacy Discovery is disabled. Please use secure connection (HTTPS) for device enrollment.");
|
||||
|
||||
if (string.IsNullOrEmpty(feature))
|
||||
{
|
||||
return Json(null);
|
||||
@@ -64,6 +75,7 @@ namespace Disco.Web.Areas.Services.Controllers
|
||||
}
|
||||
case "macenrol":
|
||||
{
|
||||
WindowsDeviceEnrolment.IncrementDiscoveryMethod(DeviceEnrolmentServerDiscoveryMethod.Mac);
|
||||
var Binder = ModelBinders.Binders.GetBinder(typeof(MacEnrol));
|
||||
var BinderContext = new ModelBindingContext()
|
||||
{
|
||||
@@ -78,6 +90,7 @@ namespace Disco.Web.Areas.Services.Controllers
|
||||
}
|
||||
case "macsecureenrol":
|
||||
{
|
||||
WindowsDeviceEnrolment.IncrementDiscoveryMethod(DeviceEnrolmentServerDiscoveryMethod.MacSecure);
|
||||
using (var database = new DiscoDataContext())
|
||||
{
|
||||
var host = HttpContext.Request.UserHostAddress;
|
||||
@@ -93,6 +106,9 @@ namespace Disco.Web.Areas.Services.Controllers
|
||||
[Authorize]
|
||||
public virtual ActionResult Authenticated(string feature)
|
||||
{
|
||||
if (!CheckLegacyEnrollmentDiscovery())
|
||||
return BadRequest("Enrollment Legacy Discovery is disabled. Please use secure connection (HTTPS) for device enrollment.");
|
||||
|
||||
if (string.IsNullOrEmpty(feature))
|
||||
{
|
||||
WhoAmIResponse whoAmIResponse = new WhoAmI().BuildResponse();
|
||||
@@ -171,5 +187,21 @@ namespace Disco.Web.Areas.Services.Controllers
|
||||
return Content("Error Message Logged");
|
||||
}
|
||||
|
||||
private bool CheckLegacyEnrollmentDiscovery()
|
||||
{
|
||||
if (!Request.IsSecureConnection)
|
||||
{
|
||||
using (DiscoDataContext database = new DiscoDataContext())
|
||||
{
|
||||
if (database.DiscoConfiguration.Devices.EnrollmentLegacyDiscoveryDisabled)
|
||||
{
|
||||
EnrolmentLog.LogClientError(Request.UserHostAddress, Request.UserHostName, string.Empty, "Enrollment Legacy Discovery is disabled. Please use secure connection (HTTPS) for device enrollment.", string.Empty);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,12 @@ namespace Disco.Web.Areas.API.Controllers
|
||||
{
|
||||
return new T4MVC_System_Web_Mvc_ActionResult(Area, Name, ActionNames.MacSshPassword);
|
||||
}
|
||||
[NonAction]
|
||||
[GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode]
|
||||
public virtual System.Web.Mvc.ActionResult LegacyDiscovery()
|
||||
{
|
||||
return new T4MVC_System_Web_Mvc_ActionResult(Area, Name, ActionNames.LegacyDiscovery);
|
||||
}
|
||||
|
||||
[GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode]
|
||||
public EnrolmentController Actions { get { return MVC.API.Enrolment; } }
|
||||
@@ -103,6 +109,7 @@ namespace Disco.Web.Areas.API.Controllers
|
||||
public readonly string PendingTimeoutMinutes = "PendingTimeoutMinutes";
|
||||
public readonly string MacSshUsername = "MacSshUsername";
|
||||
public readonly string MacSshPassword = "MacSshPassword";
|
||||
public readonly string LegacyDiscovery = "LegacyDiscovery";
|
||||
}
|
||||
|
||||
[GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode]
|
||||
@@ -112,6 +119,7 @@ namespace Disco.Web.Areas.API.Controllers
|
||||
public const string PendingTimeoutMinutes = "PendingTimeoutMinutes";
|
||||
public const string MacSshUsername = "MacSshUsername";
|
||||
public const string MacSshPassword = "MacSshPassword";
|
||||
public const string LegacyDiscovery = "LegacyDiscovery";
|
||||
}
|
||||
|
||||
|
||||
@@ -151,6 +159,14 @@ namespace Disco.Web.Areas.API.Controllers
|
||||
{
|
||||
public readonly string MacSshPassword = "MacSshPassword";
|
||||
}
|
||||
static readonly ActionParamsClass_LegacyDiscovery s_params_LegacyDiscovery = new ActionParamsClass_LegacyDiscovery();
|
||||
[GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode]
|
||||
public ActionParamsClass_LegacyDiscovery LegacyDiscoveryParams { get { return s_params_LegacyDiscovery; } }
|
||||
[GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode]
|
||||
public class ActionParamsClass_LegacyDiscovery
|
||||
{
|
||||
public readonly string enabled = "enabled";
|
||||
}
|
||||
static readonly ViewsClass s_views = new ViewsClass();
|
||||
[GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode]
|
||||
public ViewsClass Views { get { return s_views; } }
|
||||
@@ -222,6 +238,18 @@ namespace Disco.Web.Areas.API.Controllers
|
||||
return callInfo;
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
partial void LegacyDiscoveryOverride(T4MVC_System_Web_Mvc_ActionResult callInfo, bool enabled);
|
||||
|
||||
[NonAction]
|
||||
public override System.Web.Mvc.ActionResult LegacyDiscovery(bool enabled)
|
||||
{
|
||||
var callInfo = new T4MVC_System_Web_Mvc_ActionResult(Area, Name, ActionNames.LegacyDiscovery);
|
||||
ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, "enabled", enabled);
|
||||
LegacyDiscoveryOverride(callInfo, enabled);
|
||||
return callInfo;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -145,8 +145,9 @@ Global
|
||||
UpdateAssemblyVersion = True
|
||||
UpdateAssemblyFileVersion = True
|
||||
UpdateAssemblyInfoVersion = False
|
||||
AssemblyVersionSettings = None.None.DateStamp.TimeStamp
|
||||
AssemblyFileVersionSettings = None.None.DateStamp.TimeStamp
|
||||
ShouldCreateLogs = True
|
||||
AssemblyVersionSettings = None.None.DateStamp.None
|
||||
AssemblyFileVersionSettings = None.None.DateStamp.None
|
||||
UpdatePackageVersion = False
|
||||
AssemblyInfoVersionType = SettingsVersion
|
||||
InheritWinAppVersionFrom = None
|
||||
|
||||
Reference in New Issue
Block a user