Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
DomainConfiguration.cs
1using System;
3using System.Diagnostics;
4using System.IO;
5using System.Net;
6using System.Net.Http;
8using System.Security.Cryptography.X509Certificates;
9using System.Text;
10using System.Threading.Tasks;
11using Waher.Content;
15using Waher.Events;
21using Waher.Script;
22using Waher.Security;
25
27{
32 {
33 private static DomainConfiguration instance = null;
34
35 private HttpResource testDomainNames = null;
36 private HttpResource testDomainName = null;
37 private HttpResource testCA = null;
38 private HttpResource acmeChallenge = null;
39 private HttpResource saveNames = null;
40 private HttpResource saveDescriptions = null;
41
42 private AlternativeField[] localizedNames = null;
43 private AlternativeField[] localizedDescriptions = null;
44 private string[] alternativeDomains = null;
45 private byte[] certificate = null;
46 private byte[] privateKey = null;
47 private byte[] pfx = null;
48 private string humanReadableName = string.Empty;
49 private string humanReadableNameLanguage = string.Empty;
50 private string humanReadableDescription = string.Empty;
51 private string humanReadableDescriptionLanguage = string.Empty;
52 private string domain = string.Empty;
53 private string acmeDirectory = string.Empty;
54 private string contactEMail = string.Empty;
55 private string urlToS = string.Empty;
56 private string password = string.Empty;
57 private string openSslPath = string.Empty;
58 private string dynDnsTemplate = string.Empty;
59 private string checkIpScript = string.Empty;
60 private string updateIpScript = string.Empty;
61 private string dynDnsAccount = string.Empty;
62 private string dynDnsPassword = string.Empty;
63 private int dynDnsInterval = 300;
64 private bool useDomainName = false;
65 private bool dynamicDns = false;
66 private bool useEncryption = true;
67 private bool customCA = false;
68 private bool acceptToS = false;
69 private bool deferredTest = false;
70
71 private string challenge = string.Empty;
72 private string token = string.Empty;
73
77 public static DomainConfiguration Instance => instance;
78
82 [DefaultValueStringEmpty]
83 public string Domain
84 {
85 get => this.domain;
86 set => this.domain = value;
87 }
88
92 [DefaultValueNull]
93 public string[] AlternativeDomains
94 {
95 get => this.alternativeDomains;
96 set => this.alternativeDomains = value;
97 }
98
102 [DefaultValue(false)]
103 public bool UseDomainName
104 {
105 get => this.useDomainName;
106 set
107 {
108 this.useDomainName = value;
109 if (!this.useDomainName)
110 {
111 this.domain = string.Empty;
112 this.alternativeDomains = null;
113 this.dynamicDns = false;
114 this.useEncryption = false;
115 this.customCA = false;
116 }
117 }
118 }
119
123 [DefaultValue(false)]
124 public bool DynamicDns
125 {
126 get => this.dynamicDns;
127 set => this.dynamicDns = value;
128 }
129
133 [DefaultValue(true)]
134 public bool UseEncryption
135 {
136 get => this.useEncryption;
137 set => this.useEncryption = value;
138 }
139
143 [DefaultValue(false)]
144 public bool CustomCA
145 {
146 get => this.customCA;
147 set => this.customCA = value;
148 }
149
153 [DefaultValueStringEmpty]
154 public string AcmeDirectory
155 {
156 get => this.acmeDirectory;
157 set => this.acmeDirectory = value;
158 }
159
163 [DefaultValueStringEmpty]
164 public string ContactEMail
165 {
166 get => this.contactEMail;
167 set => this.contactEMail = value;
168 }
169
173 [DefaultValueStringEmpty]
174 public string UrlToS
175 {
176 get => this.urlToS;
177 set => this.urlToS = value;
178 }
179
183 [DefaultValue(false)]
184 public bool AcceptToS
185 {
186 get => this.acceptToS;
187 set => this.acceptToS = value;
188 }
189
193 [DefaultValueNull]
194 public byte[] Certificate
195 {
196 get => this.certificate;
197 set => this.certificate = value;
198 }
199
203 [DefaultValueNull]
204 public byte[] PrivateKey
205 {
206 get => this.privateKey;
207 set => this.privateKey = value;
208 }
209
213 [DefaultValueNull]
214 public byte[] PFX
215 {
216 get => this.pfx;
217 set => this.pfx = value;
218 }
219
223 [Encrypted(32)]
224 public string Password
225 {
226 get => this.password;
227 set => this.password = value;
228 }
229
233 public string[] EncryptedProperties => new string[] { nameof(this.Password) };
234
238 [DefaultValueStringEmpty]
239 public string OpenSslPath
240 {
241 get => this.openSslPath;
242 set => this.openSslPath = value;
243 }
244
248 [DefaultValueStringEmpty]
249 public string DynDnsTemplate
250 {
251 get => this.dynDnsTemplate;
252 set => this.dynDnsTemplate = value;
253 }
254
258 [DefaultValueStringEmpty]
259 public string CheckIpScript
260 {
261 get => this.checkIpScript;
262 set => this.checkIpScript = value;
263 }
264
268 [DefaultValueStringEmpty]
269 public string UpdateIpScript
270 {
271 get => this.updateIpScript;
272 set => this.updateIpScript = value;
273 }
274
278 [DefaultValueStringEmpty]
279 public string DynDnsAccount
280 {
281 get => this.dynDnsAccount;
282 set => this.dynDnsAccount = value;
283 }
284
288 [DefaultValue(300)]
289 public int DynDnsInterval
290 {
291 get => this.dynDnsInterval;
292 set => this.dynDnsInterval = value;
293 }
294
298 [DefaultValueStringEmpty]
299 public string DynDnsPassword
300 {
301 get => this.dynDnsPassword;
302 set => this.dynDnsPassword = value;
303 }
304
308 [DefaultValueStringEmpty]
309 public string HumanReadableName
310 {
311 get => this.humanReadableName;
312 set => this.humanReadableName = value;
313 }
314
318 [DefaultValueStringEmpty]
320 {
321 get => this.humanReadableNameLanguage;
322 set => this.humanReadableNameLanguage = value;
323 }
324
328 [DefaultValueStringEmpty]
330 {
331 get => this.humanReadableDescription;
332 set => this.humanReadableDescription = value;
333 }
334
338 [DefaultValueStringEmpty]
340 {
341 get => this.humanReadableDescriptionLanguage;
342 set => this.humanReadableDescriptionLanguage = value;
343 }
344
348 [DefaultValueNull]
350 {
351 get => this.localizedNames;
352 set => this.localizedNames = value;
353 }
354
358 [DefaultValueNull]
360 {
361 get => this.localizedDescriptions;
362 set => this.localizedDescriptions = value;
363 }
364
368 public bool HasToS => !string.IsNullOrEmpty(this.urlToS);
369
373 public bool HasCertificate =>
374 (!(this.certificate is null) && !(this.privateKey is null)) ||
375 (!(this.pfx is null) && !(this.password is null));
376
380 public override string Resource => "/Settings/Domain.md";
381
385 public override int Priority => 200;
386
392 public override Task<string> Title(Language Language)
393 {
394 return Language.GetStringAsync(typeof(Gateway), 3, "Domain");
395 }
396
400 public override Task ConfigureSystem()
401 {
402 return Gateway.ConfigureDomain(this);
403 }
404
409 public override void SetStaticInstance(ISystemConfiguration Configuration)
410 {
411 instance = Configuration as DomainConfiguration;
412 }
413
418 public override Task InitSetup(HttpServer WebServer)
419 {
420 this.testDomainNames = WebServer.Register("/Settings/TestDomainNames", null, this.TestDomainNames, true, false, true);
421 this.testDomainName = WebServer.Register("/Settings/TestDomainName", this.TestDomainName, true, false, true);
422 this.testCA = WebServer.Register("/Settings/TestCA", null, this.TestCA, true, false, true);
423 this.acmeChallenge = WebServer.Register("/.well-known/acme-challenge", this.AcmeChallenge, true, true, true);
424 this.saveNames = WebServer.Register("/Settings/SaveNames", null, this.SaveNames, true, false, true);
425 this.saveDescriptions = WebServer.Register("/Settings/SaveDescriptions", null, this.SaveDescriptions, true, false, true);
426
427 return base.InitSetup(WebServer);
428 }
429
434 public override Task UnregisterSetup(HttpServer WebServer)
435 {
436 WebServer.Unregister(this.testDomainNames);
437 WebServer.Unregister(this.testDomainName);
438 WebServer.Unregister(this.testCA);
439 WebServer.Unregister(this.acmeChallenge);
440 WebServer.Unregister(this.saveNames);
441 WebServer.Unregister(this.saveDescriptions);
442
443 return base.UnregisterSetup(WebServer);
444 }
445
449 protected override string ConfigPrivilege => "Admin.Communication.Domain";
450
451 private async Task TestDomainNames(HttpRequest Request, HttpResponse Response)
452 {
453 Gateway.AssertUserAuthenticated(Request, this.ConfigPrivilege);
454
455 if (!Request.HasData)
456 {
457 await Response.SendResponse(new BadRequestException());
458 return;
459 }
460
461 ContentResponse Content = await Request.DecodeDataAsync();
462 if (Content.HasError || !(Content.Decoded is Dictionary<string, object> Parameters))
463 {
464 await Response.SendResponse(new BadRequestException());
465 return;
466 }
467
468 if (!Parameters.TryGetValue("domainName", out object Obj) || !(Obj is string DomainName) ||
469 !Parameters.TryGetValue("dynamicDns", out Obj) || !(Obj is bool DynamicDns) ||
470 !Parameters.TryGetValue("dynDnsTemplate", out Obj) || !(Obj is string DynDnsTemplate) ||
471 !Parameters.TryGetValue("checkIpScript", out Obj) || !(Obj is string CheckIpScript) ||
472 !Parameters.TryGetValue("updateIpScript", out Obj) || !(Obj is string UpdateIpScript) ||
473 !Parameters.TryGetValue("dynDnsAccount", out Obj) || !(Obj is string DynDnsAccount) ||
474 !Parameters.TryGetValue("dynDnsPassword", out Obj) || !(Obj is string DynDnsPassword) ||
475 !Parameters.TryGetValue("dynDnsInterval", out Obj) || !(Obj is int DynDnsInterval))
476 {
477 await Response.SendResponse(new BadRequestException());
478 return;
479 }
480
481 if (string.Compare(DomainName, "localhost", true) == 0)
482 {
483 await Response.SendResponse(new BadRequestException("localhost is not a valid domain name."));
484 return;
485 }
486
487 List<string> AlternativeNames = new List<string>();
488 int Index = 0;
489
490 while (Parameters.TryGetValue("altDomainName" + Index.ToString(), out Obj) && Obj is string AltDomainName && !string.IsNullOrEmpty(AltDomainName))
491 {
492 if (string.Compare(AltDomainName, "localhost", true) == 0)
493 {
494 await Response.SendResponse(new BadRequestException("localhost is not a valid domain name."));
495 return;
496 }
497
498 AlternativeNames.Add(AltDomainName);
499 Index++;
500 }
501
502 if (Parameters.TryGetValue("altDomainName", out Obj) && Obj is string AltDomainName2 && !string.IsNullOrEmpty(AltDomainName2))
503 {
504 if (string.Compare(AltDomainName2, "localhost", true) == 0)
505 {
506 await Response.SendResponse(new BadRequestException("localhost is not a valid domain name."));
507 return;
508 }
509
510 AlternativeNames.Add(AltDomainName2);
511 }
512
513 string TabID = Request.Header["X-TabID"];
514 if (string.IsNullOrEmpty(TabID))
515 {
516 await Response.SendResponse(new BadRequestException());
517 return;
518 }
519
520 this.dynamicDns = DynamicDns;
521 this.dynDnsTemplate = DynDnsTemplate;
522 this.checkIpScript = CheckIpScript;
523 this.updateIpScript = UpdateIpScript;
524 this.dynDnsAccount = DynDnsAccount;
525 this.dynDnsPassword = DynDnsPassword;
526 this.dynDnsInterval = DynDnsInterval;
527 this.domain = DomainName;
528 this.alternativeDomains = AlternativeNames.Count == 0 ? null : AlternativeNames.ToArray();
529 this.useDomainName = true;
530
531 Response.StatusCode = 200;
532 Response.StatusMessage = "OK";
533
534 Task _ = Task.Run(async () => await this.Test(TabID, false));
535 }
536
537 private Task TestDomainName(HttpRequest Request, HttpResponse Response)
538 {
539 Response.StatusCode = 200;
540 Response.StatusMessage = "OK";
541 Response.ContentType = PlainTextCodec.DefaultContentType;
542 return Response.Write(this.token);
543 }
544
545 private async Task<bool> Test(string TabID, bool EnvironmentSetup)
546 {
547 try
548 {
549 if (!string.IsNullOrEmpty(this.domain))
550 {
551 if (!await this.Test(TabID, EnvironmentSetup, this.domain))
552 {
553 if (string.IsNullOrEmpty(TabID))
554 this.LogEnvironmentError("Domain name not valid.", GATEWAY_DOMAIN_NAME, this.domain);
555 else
556 await ClientEvents.PushEvent(new string[] { TabID }, "NameNotValid", this.domain, false, "User");
557 return false;
558 }
559 }
560
561 if (!(this.alternativeDomains is null))
562 {
563 foreach (string AltDomainName in this.alternativeDomains)
564 {
565 if (!await this.Test(TabID, EnvironmentSetup, AltDomainName))
566 {
567 if (string.IsNullOrEmpty(TabID))
568 this.LogEnvironmentError("Alternative domain name not valid.", GATEWAY_DOMAIN_ALT, AltDomainName);
569 else
570 await ClientEvents.PushEvent(new string[] { TabID }, "NameNotValid", AltDomainName, false, "User");
571 return false;
572 }
573 }
574 }
575
576 if (this.Step < 1)
577 this.Step = 1;
578
579 this.Updated = DateTime.Now;
580 await Database.Update(this);
581
582 if (!string.IsNullOrEmpty(TabID))
583 await ClientEvents.PushEvent(new string[] { TabID }, "NamesOK", string.Empty, false, "User");
584
585 return true;
586 }
587 catch (Exception ex)
588 {
589 if (string.IsNullOrEmpty(TabID))
590 Log.Exception(ex);
591 else
592 await ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", ex.Message, false, "User");
593
594 return false;
595 }
596 }
597
598 internal Task<bool> CheckDynamicIp()
599 {
600 return this.CheckDynamicIp(null, false);
601 }
602
603 internal Task<bool> CheckDynamicIp(bool EnvironmentSetup)
604 {
605 return this.CheckDynamicIp(null, EnvironmentSetup);
606 }
607
608 private async Task<bool> CheckDynamicIp(string TabID, bool EnvironmentSetup)
609 {
610 try
611 {
612 if (!this.useDomainName || !this.dynamicDns)
613 return true;
614
615 bool Result = true;
616
617 if (!await this.CheckDynamicIp(TabID, this.domain, EnvironmentSetup))
618 Result = false;
619
620 foreach (string AlternativeDomain in this.alternativeDomains)
621 {
622 if (!await this.CheckDynamicIp(TabID, AlternativeDomain, EnvironmentSetup))
623 Result = false;
624 }
625
626 return Result;
627 }
628 catch (Exception ex)
629 {
630 Log.Exception(ex);
631 return false;
632 }
633 }
634
635 internal async Task<bool> CheckDynamicIp(string TabID, string DomainName, bool EnvironmentSetup)
636 {
637 string Msg = await this.CheckDynamicIp(DomainName, EnvironmentSetup, async (_, Status) =>
638 {
639 if (!string.IsNullOrEmpty(TabID))
640 await ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", Status, false, "User");
641 });
642
643 if (!string.IsNullOrEmpty(Msg))
644 {
645 if (!string.IsNullOrEmpty(TabID))
646 await ClientEvents.PushEvent(new string[] { TabID }, "CertificateError", Msg, false, "User");
647
648 return false;
649 }
650
651 return true;
652 }
653
661 public async Task<string> CheckDynamicIp(string DomainName, bool EnvironmentSetup, EventHandlerAsync<string> Status)
662 {
663 if (!this.dynamicDns)
664 return null;
665
668
669 try
670 {
671 CheckIpScript = new Expression(this.checkIpScript);
672 }
673 catch (Exception ex)
674 {
675 string Msg = "Unable to parse script checking current IP Address: " + ex.Message;
676
677 if (EnvironmentSetup)
678 this.LogEnvironmentError(Msg, GATEWAY_DYNDNS_CHECK, this.checkIpScript);
679
680 return Msg;
681 }
682
683 try
684 {
685 UpdateIpScript = new Expression(this.updateIpScript);
686 }
687 catch (Exception ex)
688 {
689 string Msg = "Unable to parse script updating the dynamic DNS server: " + ex.Message;
690
691 if (EnvironmentSetup)
692 this.LogEnvironmentError(Msg, GATEWAY_DYNDNS_UPDATE, this.updateIpScript);
693
694 return Msg;
695 }
696
697 await Status.Raise(this, "Checking current IP Address.");
698
700 object Result;
701
702 try
703 {
704 Result = await CheckIpScript.EvaluateAsync(Variables);
705 }
706 catch (Exception ex)
707 {
708 string Msg = "Unable to get current IP Address: " + ex.Message;
709
710 if (EnvironmentSetup)
711 this.LogEnvironmentError(Msg, GATEWAY_DYNDNS_CHECK, this.checkIpScript);
712
713 return Msg;
714 }
715
716 if (!(Result is string CurrentIP) || !IPAddress.TryParse(CurrentIP, out IPAddress _))
717 {
718 string Msg = "Unable to get current IP Address. Unexpected response.";
719
720 if (EnvironmentSetup)
721 this.LogEnvironmentError(Msg, GATEWAY_DYNDNS_CHECK, this.checkIpScript);
722
723 return Msg;
724 }
725
726 await Status.Raise(this, "Current IP Address: " + CurrentIP);
727
728 string LastIP = await RuntimeSettings.GetAsync("Last.IP." + DomainName, string.Empty);
729
730 if (LastIP == CurrentIP)
731 await Status.Raise(this, "IP Address has not changed for " + DomainName + ".");
732 else
733 {
734 try
735 {
736 await Status.Raise(this, "Updating IP address for " + DomainName + " to " + CurrentIP + ".");
737
738 Variables["Account"] = this.dynDnsAccount;
739 Variables["Password"] = this.dynDnsPassword;
740 Variables["IP"] = CurrentIP;
741 Variables["Domain"] = DomainName;
742
743 Result = await UpdateIpScript.EvaluateAsync(Variables);
744
745 await RuntimeSettings.SetAsync("Last.IP." + DomainName, CurrentIP);
746 }
747 catch (Exception ex)
748 {
749 string Msg = "Unable to register new dynamic IP Address: " + ex.Message;
750
751 if (EnvironmentSetup)
752 this.LogEnvironmentError(Msg, GATEWAY_DYNDNS_UPDATE, this.updateIpScript);
753
754 return Msg;
755 }
756 }
757
758 return null;
759 }
760
761 private async Task<bool> Test(string TabID, bool EnvironmentSetup, string DomainName)
762 {
763 string Msg = await this.Test(DomainName, EnvironmentSetup, async (_, Status) =>
764 {
765 if (!string.IsNullOrEmpty(TabID))
766 await ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", Status, false, "User");
767 });
768
769 if (!string.IsNullOrEmpty(Msg))
770 {
771 if (!string.IsNullOrEmpty(TabID))
772 await ClientEvents.PushEvent(new string[] { TabID }, "CertificateError", Msg, false, "User");
773
774 return false;
775 }
776
777 return true;
778 }
779
787 public async Task<string> Test(string DomainName, bool EnvironmentSetup, EventHandlerAsync<string> Status)
788 {
789 await Status.Raise(this, "Testing " + DomainName + "...");
790
791 string Msg = await this.CheckDynamicIp(DomainName, EnvironmentSetup, Status);
792 if (!string.IsNullOrEmpty(Msg))
793 return Msg;
794
795 this.token = Hashes.BinaryToString(Gateway.NextBytes(32));
796
797 using (HttpClient HttpClient = new HttpClient()
798 {
799 Timeout = TimeSpan.FromMilliseconds(10000)
800 })
801 {
802 try
803 {
804 StringBuilder Url = new StringBuilder();
805 int[] HttpPorts = Gateway.GetConfigPorts("HTTP");
806
807 Url.Append("http://");
808 Url.Append(DomainName);
809
810 if (Array.IndexOf(HttpPorts, 80) < 0 && HttpPorts.Length > 0)
811 {
812 Url.Append(':');
813 Url.Append(HttpPorts[0].ToString());
814 }
815
816 Url.Append("/Settings/TestDomainName");
817
818 HttpResponseMessage Response = await HttpClient.GetAsync("http://" + DomainName + "/Settings/TestDomainName");
819 if (!Response.IsSuccessStatusCode)
820 return "Domain name does not point to this machine.";
821
822 byte[] Bin = await Response.Content.ReadAsByteArrayAsync();
823 string Token = System.Text.Encoding.ASCII.GetString(Bin);
824
825 if (Token != this.token)
826 return "Unexpected response returned. Domain name does not point to this machine.";
827 }
828 catch (TimeoutException)
829 {
830 return "Time-out. Check that the domain name points to this machine.";
831 }
832 catch (Exception ex)
833 {
834 return "Unable to validate domain name: " + ex.Message;
835 }
836 }
837
838 await Status.Raise(this, "Domain name valid.");
839
840 return null;
841 }
842
843 private async Task TestCA(HttpRequest Request, HttpResponse Response)
844 {
845 Gateway.AssertUserAuthenticated(Request, this.ConfigPrivilege);
846
847 if (!Request.HasData)
848 {
849 await Response.SendResponse(new BadRequestException());
850 return;
851 }
852
853 ContentResponse Content = await Request.DecodeDataAsync();
854 if (Content.HasError || !(Content.Decoded is Dictionary<string, object> Parameters))
855 {
856 await Response.SendResponse(new BadRequestException());
857 return;
858 }
859
860 if (!Parameters.TryGetValue("useEncryption", out object Obj) || !(Obj is bool UseEncryption))
861 {
862 await Response.SendResponse(new BadRequestException());
863 return;
864 }
865
866 if (!Parameters.TryGetValue("customCA", out Obj) || !(Obj is bool CustomCA))
867 {
868 await Response.SendResponse(new BadRequestException());
869 return;
870 }
871
872 if (!Parameters.TryGetValue("acmeDirectory", out Obj) || !(Obj is string AcmeDirectory))
873 {
874 await Response.SendResponse(new BadRequestException());
875 return;
876 }
877
878 if (!Parameters.TryGetValue("contactEMail", out Obj) || !(Obj is string ContactEMail))
879 {
880 await Response.SendResponse(new BadRequestException());
881 return;
882 }
883
884 if (!Parameters.TryGetValue("acceptToS", out Obj) || !(Obj is bool AcceptToS))
885 {
886 await Response.SendResponse(new BadRequestException());
887 return;
888 }
889
890 if (!Parameters.TryGetValue("domainName", out Obj) || !(Obj is string DomainName) ||
891 !Parameters.TryGetValue("dynamicDns", out Obj) || !(Obj is bool DynamicDns) ||
892 !Parameters.TryGetValue("dynDnsTemplate", out Obj) || !(Obj is string DynDnsTemplate) ||
893 !Parameters.TryGetValue("checkIpScript", out Obj) || !(Obj is string CheckIpScript) ||
894 !Parameters.TryGetValue("updateIpScript", out Obj) || !(Obj is string UpdateIpScript) ||
895 !Parameters.TryGetValue("dynDnsAccount", out Obj) || !(Obj is string DynDnsAccount) ||
896 !Parameters.TryGetValue("dynDnsPassword", out Obj) || !(Obj is string DynDnsPassword) ||
897 !Parameters.TryGetValue("dynDnsInterval", out Obj) || !(Obj is int DynDnsInterval))
898 {
899 await Response.SendResponse(new BadRequestException());
900 return;
901
902 }
903 List<string> AlternativeNames = new List<string>();
904 int Index = 0;
905
906 while (Parameters.TryGetValue("altDomainName" + Index.ToString(), out Obj) && Obj is string AltDomainName && !string.IsNullOrEmpty(AltDomainName))
907 {
908 AlternativeNames.Add(AltDomainName);
909 Index++;
910 }
911
912 if (Parameters.TryGetValue("altDomainName", out Obj) && Obj is string AltDomainName2 && !string.IsNullOrEmpty(AltDomainName2))
913 AlternativeNames.Add(AltDomainName2);
914
915 string TabID = Request.Header["X-TabID"];
916 if (string.IsNullOrEmpty(TabID))
917 {
918 await Response.SendResponse(new BadRequestException());
919 return;
920 }
921
922 this.dynamicDns = DynamicDns;
923 this.dynDnsTemplate = DynDnsTemplate;
924 this.checkIpScript = CheckIpScript;
925 this.updateIpScript = UpdateIpScript;
926 this.dynDnsAccount = DynDnsAccount;
927 this.dynDnsPassword = DynDnsPassword;
928 this.dynDnsInterval = DynDnsInterval;
929 this.domain = DomainName;
930 this.alternativeDomains = AlternativeNames.Count == 0 ? null : AlternativeNames.ToArray();
931 this.useDomainName = true;
932 this.useEncryption = UseEncryption;
933 this.customCA = CustomCA;
934 this.acmeDirectory = AcmeDirectory;
935 this.contactEMail = ContactEMail;
936 this.acceptToS = AcceptToS;
937
938 Response.StatusCode = 200;
939 Response.StatusMessage = "OK";
940
941 if (!this.inProgress)
942 {
943 this.inProgress = true;
944 Task _ = Task.Run(async () =>
945 {
946 try
947 {
948 await this.CreateCertificate(TabID, false);
949 }
950 catch (Exception ex)
951 {
952 Log.Exception(ex);
953 }
954 });
955 }
956 }
957
958 private bool inProgress = false;
959
966 public async Task<string> AddDomain(string Name, EventHandlerAsync<string> Status)
967 {
968 if (this.IsDomainRegistered(Name))
969 return "Domain already registered.";
970
971 string Msg = await this.Test(Name, false, Status);
972 if (!string.IsNullOrEmpty(Msg))
973 return Msg;
974
975 string DomainBak = this.domain;
976 bool UseDomainBak = this.useDomainName;
977 string[] AlternativeBak = this.alternativeDomains;
978
979 if (string.IsNullOrEmpty(this.domain))
980 {
981 this.domain = Name;
982 this.useDomainName = true;
983 }
984 else if (this.alternativeDomains is null)
985 this.alternativeDomains = new string[] { Name };
986 else
987 {
988 int c = this.alternativeDomains.Length;
989 string[] NewArray = new string[c + 1];
990 this.alternativeDomains.CopyTo(NewArray, 0);
991 NewArray[c] = Name;
992 this.alternativeDomains = NewArray;
993 }
994
995 Msg = await this.CreateCertificate(false, Status, (_, __) => Task.CompletedTask);
996 if (!string.IsNullOrEmpty(Msg))
997 {
998 this.domain = DomainBak;
999 this.useDomainName = UseDomainBak;
1000 this.alternativeDomains = AlternativeBak;
1001
1002 return Msg;
1003 }
1004
1005 this.Updated = DateTime.Now;
1006 await Database.Update(this);
1007
1008 return null;
1009 }
1010
1017 public async Task<string> RemoveDomain(string Name, EventHandlerAsync<string> Status)
1018 {
1019 if (!this.IsDomainRegistered(Name))
1020 return "Domain not registered.";
1021
1022 string DomainBak = this.domain;
1023 bool UseDomainBak = this.useDomainName;
1024 string[] AlternativeBak = this.alternativeDomains;
1025 string[] NewArray;
1026
1027 if (string.Compare(this.domain, Name, true) == 0)
1028 {
1029 if ((this.alternativeDomains?.Length ?? 0) == 0)
1030 {
1031 this.domain = string.Empty;
1032 this.useDomainName = false;
1033 }
1034 else
1035 {
1036 this.domain = this.alternativeDomains[0];
1037
1038 int c = this.alternativeDomains.Length;
1039
1040 if (c == 1)
1041 this.alternativeDomains = null;
1042 else
1043 {
1044 NewArray = new string[c - 1];
1045 Array.Copy(this.alternativeDomains, 1, NewArray, 0, c - 1);
1046 this.alternativeDomains = NewArray;
1047 }
1048 }
1049 }
1050 else if (!(this.alternativeDomains is null))
1051 {
1052 int c = this.alternativeDomains.Length;
1053 if (c == 1)
1054 this.alternativeDomains = null;
1055 else
1056 {
1057 int i = Array.IndexOf(this.alternativeDomains, Name);
1058
1059 NewArray = new string[c - 1];
1060
1061 if (i > 0)
1062 Array.Copy(this.alternativeDomains, 0, NewArray, 0, i);
1063
1064 if (i < c - 1)
1065 Array.Copy(this.alternativeDomains, i + 1, NewArray, i, c - i - 1);
1066
1067 this.alternativeDomains = NewArray;
1068 }
1069 }
1070
1071 if (this.useDomainName)
1072 {
1073 string Msg = await this.CreateCertificate(false, Status, (_, __) => Task.CompletedTask);
1074 if (!string.IsNullOrEmpty(Msg))
1075 {
1076 this.domain = DomainBak;
1077 this.useDomainName = UseDomainBak;
1078 this.alternativeDomains = AlternativeBak;
1079
1080 return Msg;
1081 }
1082 }
1083
1084 this.Updated = DateTime.Now;
1085 await Database.Update(this);
1086
1087 return null;
1088 }
1089
1095 public bool IsDomainRegistered(string Name)
1096 {
1097 if (string.Compare(Name, this.domain, true) == 0)
1098 return true;
1099
1100 if (this.alternativeDomains is null)
1101 return false;
1102
1103 foreach (string Alternative in this.alternativeDomains)
1104 {
1105 if (string.Compare(Name, Alternative, true) == 0)
1106 return true;
1107 }
1108
1109 return false;
1110 }
1111
1112 internal Task<bool> CreateCertificate()
1113 {
1114 return this.CreateCertificate(null, false);
1115 }
1116
1117 private async Task<bool> CreateCertificate(string TabID, bool EnvironmentSetup)
1118 {
1119 try
1120 {
1121 string Msg = await this.CreateCertificate(EnvironmentSetup,
1122 async (_, Status) =>
1123 {
1124 if (!string.IsNullOrEmpty(TabID))
1125 await ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", Status, false, "User");
1126 },
1127 async (_, URL) =>
1128 {
1129 if (!string.IsNullOrEmpty(TabID))
1130 await ClientEvents.PushEvent(new string[] { TabID }, "TermsOfService", URL, false, "User");
1131 });
1132
1133 if (string.IsNullOrEmpty(Msg))
1134 {
1135 if (!string.IsNullOrEmpty(TabID))
1136 await ClientEvents.PushEvent(new string[] { TabID }, "CertificateOk", string.Empty, false, "User");
1137
1138 return true;
1139 }
1140 else
1141 {
1142 if (!string.IsNullOrEmpty(TabID))
1143 await ClientEvents.PushEvent(new string[] { TabID }, "CertificateError", Msg, false, "User");
1144
1145 return false;
1146 }
1147 }
1148 catch (Exception ex)
1149 {
1150 if (!string.IsNullOrEmpty(TabID))
1151 await ClientEvents.PushEvent(new string[] { TabID }, "CertificateError", ex.Message, false, "User");
1152
1153 if (EnvironmentSetup)
1154 this.LogEnvironmentError(ex.Message, GATEWAY_ENCRYPTION, this.useEncryption);
1155
1156 return false;
1157 }
1158 }
1159
1160 internal async Task<string> CreateCertificate(bool EnvironmentSetup, EventHandlerAsync<string> Status,
1161 EventHandlerAsync<string> TermsOfService)
1162 {
1163 try
1164 {
1165 string URL = this.customCA ? this.acmeDirectory : "https://acme-v02.api.letsencrypt.org/directory";
1166 RSAParameters Parameters;
1167 CspParameters CspParams = new CspParameters()
1168 {
1169 Flags = CspProviderFlags.UseMachineKeyStore,
1170 KeyContainerName = "IoTGateway:" + URL
1171 };
1172
1173 try
1174 {
1175 bool Ok;
1176
1177 using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(4096, CspParams))
1178 {
1179 Parameters = RSA.ExportParameters(true);
1180
1181 if (RSA.KeySize < 4096)
1182 {
1183 RSA.PersistKeyInCsp = false;
1184 RSA.Clear();
1185 Ok = false;
1186 }
1187 else
1188 Ok = true;
1189 }
1190
1191 if (!Ok)
1192 {
1193 using RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(4096, CspParams);
1194 Parameters = RSA.ExportParameters(true);
1195 }
1196 }
1197 catch (CryptographicException ex)
1198 {
1199 throw new CryptographicException("Unable to get access to cryptographic key for \"IoTGateway:" + URL +
1200 "\". Was the database created using another user?", ex);
1201 }
1202
1203 using AcmeClient Client = new AcmeClient(new Uri(URL), Parameters);
1204 await Status.Raise(this, "Connecting to directory.");
1205
1206 AcmeDirectory AcmeDirectory = await Client.GetDirectory();
1207
1209 await Status.Raise(this, "An external account is required.");
1210
1211 if (!(AcmeDirectory.TermsOfService is null))
1212 {
1213 URL = AcmeDirectory.TermsOfService.ToString();
1214 await Status.Raise(this, "Terms of service available on: " + URL);
1215 await TermsOfService.Raise(this, URL);
1216
1217 this.urlToS = URL;
1218
1219 if (!this.acceptToS)
1220 {
1221 string Msg = "You need to accept the terms of service.";
1222
1223 if (EnvironmentSetup)
1224 this.LogEnvironmentError(Msg, GATEWAY_ACME_ACCEPT_TOS, this.acceptToS);
1225
1226 return Msg;
1227 }
1228 }
1229
1230 if (!(AcmeDirectory.Website is null))
1231 await Status.Raise(this, "Web site available on: " + AcmeDirectory.Website.ToString());
1232
1233 await Status.Raise(this, "Getting account.");
1234
1235 List<string> Names = new List<string>();
1236
1237 if (!string.IsNullOrEmpty(this.domain))
1238 Names.Add(this.domain);
1239
1240 if (!(this.alternativeDomains is null))
1241 {
1242 foreach (string Name in this.alternativeDomains)
1243 {
1244 if (!Names.Contains(Name))
1245 Names.Add(Name);
1246 }
1247 }
1248 string[] DomainNames = Names.ToArray();
1249
1250 AcmeAccount Account;
1251
1252 try
1253 {
1254 Account = await Client.GetAccount();
1255
1256 await Status.Raise(this, "Account found.");
1257 await Status.Raise(this, "Created: " + Account.CreatedAt.ToString());
1258 await Status.Raise(this, "Initial IP: " + Account.InitialIp);
1259 await Status.Raise(this, "Status: " + Account.Status.ToString());
1260
1261 if (string.IsNullOrEmpty(this.contactEMail))
1262 {
1263 if (!(Account.Contact is null) && Account.Contact.Length != 0)
1264 {
1265 await Status.Raise(this, "Updating contact URIs in account.");
1266 Account = await Account.Update(Array.Empty<string>());
1267 await Status.Raise(this, "Account updated.");
1268 }
1269 }
1270 else
1271 {
1272 if (Account.Contact is null || Account.Contact.Length != 1 || Account.Contact[0] != "mailto:" + this.contactEMail)
1273 {
1274 await Status.Raise(this, "Updating contact URIs in account.");
1275 Account = await Account.Update(new string[] { "mailto:" + this.contactEMail });
1276 await Status.Raise(this, "Account updated.");
1277 }
1278 }
1279 }
1281 {
1282 await Status.Raise(this, "Account not found.");
1283 await Status.Raise(this, "Creating account.");
1284
1285 Account = await Client.CreateAccount(string.IsNullOrEmpty(this.contactEMail) ? Array.Empty<string>() : new string[] { "mailto:" + this.contactEMail },
1286 this.acceptToS);
1287
1288 await Status.Raise(this, "Account created.");
1289 await Status.Raise(this, "Status: " + Account.Status.ToString());
1290 }
1291
1292 await Status.Raise(this, "Generating new key.");
1293 await Account.NewKey();
1294
1295 using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(4096, CspParams))
1296 {
1297 RSA.ImportParameters(Client.ExportAccountKey(true));
1298 }
1299
1300 await Status.Raise(this, "New key generated.");
1301
1302 await Status.Raise(this, "Creating order.");
1303 AcmeOrder Order;
1304
1305 try
1306 {
1307 Order = await Account.OrderCertificate(DomainNames, null, null);
1308 }
1309 catch (AcmeMalformedException) // Not sure why this is necessary. Perhaps because it takes time to propagate the keys correctly on the remote end?
1310 {
1311 await Task.Delay(5000);
1312 await Status.Raise(this, "Retrying.");
1313 Order = await Account.OrderCertificate(DomainNames, null, null);
1314 }
1315
1316 await Status.Raise(this, "Order created.");
1317
1318 AcmeAuthorization[] Authorizations;
1319
1320 await Status.Raise(this, "Getting authorizations.");
1321 try
1322 {
1323 Authorizations = await Order.GetAuthorizations();
1324 }
1325 catch (AcmeMalformedException) // Not sure why this is necessary. Perhaps because it takes time to propagate the keys correctly on the remote end?
1326 {
1327 await Task.Delay(5000);
1328 await Status.Raise(this, "Retrying.");
1329 Authorizations = await Order.GetAuthorizations();
1330 }
1331
1332 foreach (AcmeAuthorization Authorization in Authorizations)
1333 {
1334 await Status.Raise(this, "Processing authorization for " + Authorization.Value);
1335
1336 AcmeChallenge Challenge;
1337 bool Acknowledged = false;
1338 int Index = 1;
1339 int NrChallenges = Authorization.Challenges.Length;
1340
1341 for (Index = 1; Index <= NrChallenges; Index++)
1342 {
1343 Challenge = Authorization.Challenges[Index - 1];
1344
1345 if (Challenge is AcmeHttpChallenge HttpChallenge)
1346 {
1347 this.challenge = "/" + HttpChallenge.Token;
1348 this.token = HttpChallenge.KeyAuthorization;
1349
1350 await Status.Raise(this, "Acknowleding challenge.");
1351
1352 string Msg = await this.CheckDynamicIp(Authorization.Value, EnvironmentSetup, Status);
1353 if (string.IsNullOrEmpty(Msg))
1354 {
1355 Challenge = await HttpChallenge.AcknowledgeChallenge();
1356 await Status.Raise(this, "Challenge acknowledged: " + Challenge.Status.ToString());
1357
1358 Acknowledged = true;
1359 }
1360 else
1361 return Msg;
1362 }
1363 }
1364
1365 if (!Acknowledged)
1366 {
1367 string Msg = "No automated method found to respond to any of the authorization challenges.";
1368
1369 if (EnvironmentSetup)
1370 this.LogEnvironmentError(Msg, GATEWAY_ENCRYPTION, this.useEncryption);
1371
1372 return Msg;
1373 }
1374
1375 AcmeAuthorization Authorization2 = Authorization;
1376
1377 do
1378 {
1379 await Status.Raise(this, "Waiting to poll authorization status.");
1380 await Task.Delay(5000);
1381
1382 await Status.Raise(this, "Polling authorization.");
1383 Authorization2 = await Authorization2.Poll();
1384
1385 await Status.Raise(this, "Authorization polled: " + Authorization2.Status.ToString());
1386 }
1387 while (Authorization2.Status == AcmeAuthorizationStatus.pending);
1388
1389 if (Authorization2.Status != AcmeAuthorizationStatus.valid)
1390 {
1391 throw Authorization2.Status switch
1392 {
1393 AcmeAuthorizationStatus.deactivated => new Exception("Authorization deactivated."),
1394 AcmeAuthorizationStatus.expired => new Exception("Authorization expired."),
1395 AcmeAuthorizationStatus.invalid => new Exception("Authorization invalid."),
1396 AcmeAuthorizationStatus.revoked => new Exception("Authorization revoked."),
1397 _ => new Exception("Authorization not validated."),
1398 };
1399 }
1400 }
1401
1402 using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(4096)) // TODO: Make configurable
1403 {
1404 await Status.Raise(this, "Finalizing order.");
1405
1406 SignatureAlgorithm SignAlg = new RsaSha256(RSA);
1407
1408 Order = await Order.FinalizeOrder(new Security.PKCS.CertificateRequest(SignAlg)
1409 {
1410 CommonName = this.domain,
1411 SubjectAlternativeNames = DomainNames,
1412 EMailAddress = this.contactEMail
1413 });
1414
1415 await Status.Raise(this, "Order finalized: " + Order.Status.ToString());
1416
1417 if (Order.Status != AcmeOrderStatus.valid)
1418 {
1419 throw Order.Status switch
1420 {
1421 AcmeOrderStatus.invalid => new Exception("Order invalid."),
1422 _ => new Exception("Unable to validate order."),
1423 };
1424 }
1425
1426 if (Order.Certificate is null)
1427 throw new Exception("No certificate URI provided.");
1428
1429 await Status.Raise(this, "Downloading certificate.");
1430
1431 X509Certificate2[] Certificates = await Order.DownloadCertificate();
1432 X509Certificate2 Certificate = Certificates[0];
1433
1434 await Status.Raise(this, "Exporting certificate.");
1435
1436 this.certificate = Certificate.Export(X509ContentType.Cert);
1437 this.privateKey = RSA.ExportCspBlob(true);
1438 this.pfx = null;
1439 this.password = string.Empty;
1440
1441 await Status.Raise(this, "Adding private key.");
1442
1443 try
1444 {
1445 Certificate.PrivateKey = RSA;
1446 }
1447 catch (PlatformNotSupportedException)
1448 {
1449 await Status.Raise(this, "Platform does not support adding of private key.");
1450 await Status.Raise(this, "Searching for OpenSSL on machine.");
1451
1452 string[] Files;
1453 string Password = Hashes.BinaryToString(Gateway.NextBytes(32));
1454 string CertFileName = null;
1455 string CertFileName2 = null;
1456 string KeyFileName = null;
1457
1458 if (string.IsNullOrEmpty(this.openSslPath) || !File.Exists(this.openSslPath))
1459 {
1460 string[] Folders = Gateway.GetFolders(new Environment.SpecialFolder[]
1461 {
1462 Environment.SpecialFolder.ProgramFiles,
1463 Environment.SpecialFolder.ProgramFilesX86
1464 },
1465 Path.DirectorySeparatorChar + "OpenSSL-Win32",
1466 Path.DirectorySeparatorChar + "OpenSSL-Win64");
1467
1468 Files = Gateway.FindFiles(Folders, "openssl.exe", 2, int.MaxValue);
1469 }
1470 else
1471 Files = new string[] { this.openSslPath };
1472
1473 try
1474 {
1475 if (Files.Length == 0)
1476 {
1477 string Msg = "Unable to join certificate with private key. Try installing <a target=\"_blank\" href=\"https://wiki.openssl.org/index.php/Binaries\">OpenSSL</a> and try again.";
1478
1479 if (EnvironmentSetup)
1480 this.LogEnvironmentError(Msg, GATEWAY_ENCRYPTION, this.useEncryption);
1481
1482 return Msg;
1483 }
1484 else
1485 {
1486 foreach (string OpenSslFile in Files)
1487 {
1488 if (CertFileName is null)
1489 {
1490 await Status.Raise(this, "Generating temporary certificate file.");
1491
1492 StringBuilder PemOutput = new StringBuilder();
1493 byte[] Bin = Certificate.Export(X509ContentType.Cert);
1494
1495 PemOutput.AppendLine("-----BEGIN CERTIFICATE-----");
1496 PemOutput.AppendLine(Convert.ToBase64String(Bin, Base64FormattingOptions.InsertLineBreaks));
1497 PemOutput.AppendLine("-----END CERTIFICATE-----");
1498
1499 CertFileName = Path.Combine(Gateway.AppDataFolder, "Certificate.pem");
1500 await Runtime.IO.Files.WriteAllTextAsync(CertFileName, PemOutput.ToString(), System.Text.Encoding.ASCII);
1501
1502 await Status.Raise(this, "Generating temporary key file.");
1503
1504 DerEncoder KeyOutput = new DerEncoder();
1505 SignAlg.ExportPrivateKey(KeyOutput);
1506
1507 PemOutput.Clear();
1508 PemOutput.AppendLine("-----BEGIN RSA PRIVATE KEY-----");
1509 PemOutput.AppendLine(Convert.ToBase64String(KeyOutput.ToArray(), Base64FormattingOptions.InsertLineBreaks));
1510 PemOutput.AppendLine("-----END RSA PRIVATE KEY-----");
1511
1512 KeyFileName = Path.Combine(Gateway.AppDataFolder, "Certificate.key");
1513
1514 await Runtime.IO.Files.WriteAllTextAsync(KeyFileName, PemOutput.ToString(), System.Text.Encoding.ASCII);
1515 }
1516
1517 await Status.Raise(this, "Converting to PFX using " + OpenSslFile);
1518
1519 Process P = new Process()
1520 {
1521 StartInfo = new ProcessStartInfo()
1522 {
1523 FileName = OpenSslFile,
1524 Arguments = "pkcs12 -nodes -export -out Certificate.pfx -inkey Certificate.key -in Certificate.pem -password pass:" + Password,
1525 UseShellExecute = false,
1526 RedirectStandardError = true,
1527 RedirectStandardOutput = true,
1528 WorkingDirectory = Gateway.AppDataFolder,
1529 CreateNoWindow = true,
1530 WindowStyle = ProcessWindowStyle.Hidden
1531 }
1532 };
1533
1534 P.Start();
1535
1536 if (!P.WaitForExit(60000) || P.ExitCode != 0)
1537 {
1538 if (!P.StandardOutput.EndOfStream)
1539 await Status.Raise(this, "Output: " + P.StandardOutput.ReadToEnd());
1540
1541 if (!P.StandardError.EndOfStream)
1542 await Status.Raise(this, "Error: " + P.StandardError.ReadToEnd());
1543
1544 continue;
1545 }
1546
1547 await Status.Raise(this, "Loading PFX.");
1548
1549 CertFileName2 = Path.Combine(Gateway.AppDataFolder, "Certificate.pfx");
1550 this.pfx = await Runtime.IO.Files.ReadAllBytesAsync(CertFileName2);
1551 this.password = Password;
1552 this.openSslPath = OpenSslFile;
1553
1554 await Status.Raise(this, "PFX successfully generated using OpenSSL.");
1555 break;
1556 }
1557
1558 if (this.pfx is null)
1559 {
1560 this.openSslPath = string.Empty;
1561
1562 string Msg = "Unable to convert to PFX using OpenSSL.";
1563
1564 if (EnvironmentSetup)
1565 this.LogEnvironmentError(Msg, GATEWAY_ENCRYPTION, this.useEncryption);
1566
1567 return Msg;
1568 }
1569 }
1570 }
1571 finally
1572 {
1573 if (!(CertFileName is null) && File.Exists(CertFileName))
1574 {
1575 await Status.Raise(this, "Deleting temporary certificate file.");
1576 File.Delete(CertFileName);
1577 }
1578
1579 if (!(KeyFileName is null) && File.Exists(KeyFileName))
1580 {
1581 await Status.Raise(this, "Deleting temporary key file.");
1582 File.Delete(KeyFileName);
1583 }
1584
1585 if (!(CertFileName2 is null) && File.Exists(CertFileName2))
1586 {
1587 await Status.Raise(this, "Deleting temporary pfx file.");
1588 File.Delete(CertFileName2);
1589 }
1590 }
1591 }
1592
1593
1594 if (this.Step < 2)
1595 this.Step = 2;
1596
1597 this.Updated = DateTime.Now;
1598 await Database.Update(this);
1599
1600 await Gateway.UpdateCertificate(this);
1601
1602 return null;
1603 }
1604 }
1605 catch (Exception ex)
1606 {
1607 Log.Exception(ex);
1608
1609 string Msg = "Unable to create certificate: " + XML.HtmlValueEncode(ex.Message);
1610
1611 if (EnvironmentSetup)
1612 this.LogEnvironmentError(Msg, GATEWAY_ENCRYPTION, this.useEncryption);
1613
1614 return Msg;
1615 }
1616 finally
1617 {
1618 this.inProgress = false;
1619 }
1620 }
1621
1622 private async Task AcmeChallenge(HttpRequest Request, HttpResponse Response)
1623 {
1624 if (Request.SubPath != this.challenge)
1625 {
1626 await Response.SendResponse(new NotFoundException("ACME Challenge not found."));
1627 return;
1628 }
1629
1630 Response.StatusCode = 200;
1631 Response.StatusMessage = "OK";
1632 Response.ContentType = BinaryCodec.DefaultContentType;
1633
1634 await Response.Write(true, System.Text.Encoding.ASCII.GetBytes(this.token));
1635 }
1636
1637 private async Task SaveNames(HttpRequest Request, HttpResponse Response)
1638 {
1639 Gateway.AssertUserAuthenticated(Request, this.ConfigPrivilege);
1640
1641 if (!Request.HasData)
1642 {
1643 await Response.SendResponse(new BadRequestException());
1644 return;
1645 }
1646
1647 ContentResponse Content = await Request.DecodeDataAsync();
1648 if (!(Content.Decoded is Dictionary<string, object> Parameters))
1649 {
1650 await Response.SendResponse(new BadRequestException());
1651 return;
1652 }
1653
1654 if (!Parameters.TryGetValue("humanReadableName", out object Obj) ||
1655 !(Obj is string HumanReadableName) ||
1656 !Parameters.TryGetValue("humanReadableNameLanguage", out Obj) ||
1657 !(Obj is string HumanReadableNameLanguage))
1658 {
1659 await Response.SendResponse(new BadRequestException());
1660 return;
1661 }
1662
1663 List<AlternativeField> LocalizedNames = new List<AlternativeField>();
1664 int Index = 1;
1665
1666 while (Parameters.TryGetValue("nameLanguage" + Index.ToString(), out Obj) &&
1667 Obj is string NameLanguage &&
1668 Parameters.TryGetValue("nameLocalized" + Index.ToString(), out Obj) &&
1669 Obj is string NameLocalized)
1670 {
1671 if (string.IsNullOrEmpty(NameLanguage))
1672 {
1673 await Response.SendResponse(new BadRequestException("Language cannot be empty."));
1674 return;
1675 }
1676
1677 if (string.IsNullOrEmpty(NameLocalized))
1678 {
1679 await Response.SendResponse(new BadRequestException("Localized name cannot be empty."));
1680 return;
1681 }
1682
1683 LocalizedNames.Add(new AlternativeField(NameLanguage, NameLocalized));
1684 Index++;
1685 }
1686
1687 this.HumanReadableName = HumanReadableName;
1688 this.HumanReadableNameLanguage = HumanReadableNameLanguage;
1689 this.LocalizedNames = LocalizedNames.ToArray();
1690
1691 this.Updated = DateTime.Now;
1692 await Database.Update(this);
1693 }
1694
1695 private async Task SaveDescriptions(HttpRequest Request, HttpResponse Response)
1696 {
1697 Gateway.AssertUserAuthenticated(Request, this.ConfigPrivilege);
1698
1699 if (!Request.HasData)
1700 {
1701 await Response.SendResponse(new BadRequestException());
1702 return;
1703 }
1704
1705 ContentResponse Content = await Request.DecodeDataAsync();
1706 if (Content.HasError || !(Content.Decoded is Dictionary<string, object> Parameters))
1707 {
1708 await Response.SendResponse(new BadRequestException());
1709 return;
1710 }
1711
1712 if (!Parameters.TryGetValue("humanReadableDescription", out object Obj) ||
1713 !(Obj is string HumanReadableDescription) ||
1714 !Parameters.TryGetValue("humanReadableDescriptionLanguage", out Obj) ||
1715 !(Obj is string HumanReadableDescriptionLanguage))
1716 {
1717 await Response.SendResponse(new BadRequestException());
1718 return;
1719 }
1720
1721 List<AlternativeField> LocalizedDescriptions = new List<AlternativeField>();
1722 int Index = 1;
1723
1724 while (Parameters.TryGetValue("descriptionLanguage" + Index.ToString(), out Obj) &&
1725 Obj is string DescriptionLanguage &&
1726 Parameters.TryGetValue("descriptionLocalized" + Index.ToString(), out Obj) &&
1727 Obj is string DescriptionLocalized)
1728 {
1729 if (string.IsNullOrEmpty(DescriptionLanguage))
1730 {
1731 await Response.SendResponse(new BadRequestException("Language cannot be empty."));
1732 return;
1733 }
1734
1735 if (string.IsNullOrEmpty(DescriptionLocalized))
1736 {
1737 await Response.SendResponse(new BadRequestException("Localized description cannot be empty."));
1738 return;
1739 }
1740
1741 LocalizedDescriptions.Add(new AlternativeField(DescriptionLanguage, DescriptionLocalized));
1742 Index++;
1743 }
1744
1745 this.HumanReadableDescription = HumanReadableDescription;
1746 this.HumanReadableDescriptionLanguage = HumanReadableDescriptionLanguage;
1747 this.LocalizedDescriptions = LocalizedDescriptions.ToArray();
1748
1749 this.Updated = DateTime.Now;
1750 await Database.Update(this);
1751 }
1752
1757 public override Task<bool> SimplifiedConfiguration()
1758 {
1759 return Task.FromResult(true);
1760 }
1761
1765 public const string GATEWAY_DOMAIN_USE = nameof(GATEWAY_DOMAIN_USE);
1766
1770 public const string GATEWAY_DOMAIN_NAME = nameof(GATEWAY_DOMAIN_NAME);
1771
1772 // If a Domain Name is configured(<see cref="GATEWAY_DOMAIN"/> variable), the following variables define its operation:
1773
1777 public const string GATEWAY_DOMAIN_ALT = nameof(GATEWAY_DOMAIN_ALT);
1778
1782 public const string GATEWAY_DYNDNS = nameof(GATEWAY_DYNDNS);
1783
1787 public const string GATEWAY_ENCRYPTION = nameof(GATEWAY_ENCRYPTION);
1788
1792 public const string GATEWAY_CA_CUSTOM = nameof(GATEWAY_CA_CUSTOM);
1793
1797 public const string GATEWAY_ACME_EMAIL = nameof(GATEWAY_ACME_EMAIL);
1798
1802 public const string GATEWAY_ACME_ACCEPT_TOS = nameof(GATEWAY_ACME_ACCEPT_TOS);
1803
1807 public const string GATEWAY_HR_NAME = nameof(GATEWAY_HR_NAME);
1808
1812 public const string GATEWAY_HR_NAME_LANG = nameof(GATEWAY_HR_NAME_LANG);
1813
1817 public const string GATEWAY_HR_DESC = nameof(GATEWAY_HR_DESC);
1818
1822 public const string GATEWAY_HR_DESC_LANG = nameof(GATEWAY_HR_DESC_LANG);
1823
1827 public const string GATEWAY_HR_NAME_LOC = nameof(GATEWAY_HR_NAME_LOC);
1828
1832 public const string GATEWAY_HR_NAME_ = nameof(GATEWAY_HR_NAME_);
1833
1837 public const string GATEWAY_HR_DESC_LOC = nameof(GATEWAY_HR_DESC_LOC);
1838
1842 public const string GATEWAY_HR_DESC_ = nameof(GATEWAY_HR_DESC_);
1843
1844 // If Dynamic DNS is configured (<see cref="GATEWAY_DYNDNS"/> variable), the following variables define its operation:
1845
1849 public const string GATEWAY_DYNDNS_TEMPLATE = nameof(GATEWAY_DYNDNS_TEMPLATE);
1850
1854 public const string GATEWAY_DYNDNS_CHECK = nameof(GATEWAY_DYNDNS_CHECK);
1855
1859 public const string GATEWAY_DYNDNS_UPDATE = nameof(GATEWAY_DYNDNS_UPDATE);
1860
1864 public const string GATEWAY_DYNDNS_ACCOUNT = nameof(GATEWAY_DYNDNS_ACCOUNT);
1865
1869 public const string GATEWAY_DYNDNS_PASSWORD = nameof(GATEWAY_DYNDNS_PASSWORD);
1870
1874 public const string GATEWAY_DYNDNS_INTERVAL = nameof(GATEWAY_DYNDNS_INTERVAL);
1875
1876 // If a Custom Certificate Authority is configured(<see cref="GATEWAY_CA_CUSTOM"/> variable), the following variables define its operation:
1877
1881 public const string GATEWAY_ACME_DIRECTORY = nameof(GATEWAY_ACME_DIRECTORY);
1882
1887 public override async Task<bool> EnvironmentConfiguration()
1888 {
1889 if (!this.TryGetEnvironmentVariable(GATEWAY_DOMAIN_USE, false, out this.useDomainName))
1890 return false;
1891
1892 if (!this.useDomainName)
1893 {
1894 this.Domain = string.Empty;
1895 this.DynamicDns = false;
1896 this.UseEncryption = false;
1897 this.CustomCA = false;
1898 this.ContactEMail = string.Empty;
1899 this.AcceptToS = false;
1900 }
1901 else
1902 {
1903 if (!this.TryGetEnvironmentVariable(GATEWAY_DOMAIN_NAME, true, out string Value) ||
1904 string.IsNullOrEmpty(Value))
1905 {
1906 return !this.useDomainName;
1907 }
1908
1909 if (string.Compare(Value, "localhost", true) == 0)
1910 {
1911 this.LogEnvironmentError("localhost is not a valid domain name.", GATEWAY_DOMAIN_NAME, Value);
1912 return false;
1913 }
1914
1915 this.Domain = Value;
1916
1917 Value = Environment.GetEnvironmentVariable(GATEWAY_DOMAIN_ALT);
1918 if (string.IsNullOrEmpty(Value))
1919 this.AlternativeDomains = null;
1920 else
1921 {
1922 string[] Parts = Value.Split(',');
1923
1924 foreach (string Part in Parts)
1925 {
1926 if (string.Compare(Part, "localhost", true) == 0)
1927 {
1928 this.LogEnvironmentError("localhost is not a valid alternative domain name.", GATEWAY_DOMAIN_ALT, Value);
1929 return false;
1930 }
1931 }
1932
1933 this.AlternativeDomains = Parts;
1934 }
1935
1936 if (!this.TryGetEnvironmentVariable(GATEWAY_DYNDNS, out this.dynamicDns, false))
1937 return false;
1938
1939 if (this.dynamicDns)
1940 {
1941 if (!this.TryGetEnvironmentVariable(GATEWAY_DYNDNS_TEMPLATE, true, out Value))
1942 return false;
1943
1944 this.DynDnsTemplate = Value;
1945
1946 if (!this.TryGetEnvironmentVariable(GATEWAY_DYNDNS_CHECK, true, out Value))
1947 return false;
1948
1949 this.CheckIpScript = Value;
1950
1951 if (!this.TryGetEnvironmentVariable(GATEWAY_DYNDNS_UPDATE, true, out Value))
1952 return false;
1953
1954 this.UpdateIpScript = Value;
1955
1956 this.TryGetEnvironmentVariable(GATEWAY_DYNDNS_ACCOUNT, false, out this.dynDnsAccount);
1957 this.TryGetEnvironmentVariable(GATEWAY_DYNDNS_PASSWORD, false, out this.dynDnsPassword);
1958
1959 if (!this.TryGetEnvironmentVariable(GATEWAY_DYNDNS_UPDATE, 60, 86400, true, ref this.dynDnsInterval))
1960 return false;
1961 }
1962
1963 if (!this.TryGetEnvironmentVariable(GATEWAY_ENCRYPTION, true, out this.useEncryption))
1964 return false;
1965
1966 if (this.useEncryption)
1967 {
1968 if (!this.TryGetEnvironmentVariable(GATEWAY_CA_CUSTOM, out this.customCA, false))
1969 return false;
1970
1971 if (this.customCA)
1972 {
1973 if (!this.TryGetEnvironmentVariable(GATEWAY_ACME_DIRECTORY, true, out this.acmeDirectory))
1974 return false;
1975 }
1976
1977 if (!this.TryGetEnvironmentVariable(GATEWAY_ACME_EMAIL, true, out this.contactEMail))
1978 return false;
1979
1980 if (!this.TryGetEnvironmentVariable(GATEWAY_ACME_ACCEPT_TOS, true, out this.acceptToS))
1981 return false;
1982 }
1983 }
1984
1985 AlternativeField LocalizedString = this.GetLocalizedEnvironmentVariable(GATEWAY_HR_NAME, GATEWAY_HR_NAME_LANG);
1986 if (LocalizedString is null)
1987 return false;
1988
1989 this.HumanReadableName = LocalizedString.Value;
1990 this.HumanReadableNameLanguage = LocalizedString.Key;
1991
1992 LocalizedString = this.GetLocalizedEnvironmentVariable(GATEWAY_HR_DESC, GATEWAY_HR_DESC_LANG);
1993 if (LocalizedString is null)
1994 return false;
1995
1996 this.HumanReadableDescription = LocalizedString.Value;
1997 this.HumanReadableDescriptionLanguage = LocalizedString.Key;
1998
1999 this.localizedNames = this.GetLocalizedEnvironmentVariables(GATEWAY_HR_NAME_LOC, GATEWAY_HR_NAME_);
2000 if (this.localizedNames is null)
2001 return false;
2002
2003 this.localizedDescriptions = this.GetLocalizedEnvironmentVariables(GATEWAY_HR_DESC_LOC, GATEWAY_HR_DESC_);
2004 if (this.localizedDescriptions is null)
2005 return false;
2006
2007 if (Gateway.HttpServer is null)
2008 this.deferredTest = true;
2009 else
2010 {
2011 if (!await this.Test(null, true)) // Tests domain names.
2012 return false;
2013 }
2014
2015 if (this.dynamicDns && !await this.CheckDynamicIp(true)) // Tests dynamic DNS settings.
2016 return false;
2017
2018 if (this.useEncryption && this.useDomainName && !await this.CreateCertificate(null, true)) // Creates certificate.
2019 return false;
2020
2021 return true;
2022 }
2023
2024 private AlternativeField GetLocalizedEnvironmentVariable(string VariableName, string LanguageVariableName)
2025 {
2026 if (!this.TryGetEnvironmentVariable(VariableName, true, out string Value) ||
2027 !this.TryGetEnvironmentVariable(LanguageVariableName, true, out string Language))
2028 {
2029 return null;
2030 }
2031
2032 return new AlternativeField(Value, Language);
2033 }
2034
2035 private AlternativeField[] GetLocalizedEnvironmentVariables(string CollectionVariableName, string VariableName)
2036 {
2037 List<AlternativeField> Result = new List<AlternativeField>();
2038
2039 string Value = Environment.GetEnvironmentVariable(CollectionVariableName);
2040 if (!string.IsNullOrEmpty(Value))
2041 {
2042 string[] Languages = Value.Split(',');
2043 string Name;
2044
2045 foreach (string Language in Languages)
2046 {
2047 Name = VariableName + Language;
2048
2049 if (!this.TryGetEnvironmentVariable(Name, true, out Value))
2050 return null;
2051
2052 Result.Add(new AlternativeField(Language, Value));
2053 }
2054 }
2055
2056 return Result.ToArray();
2057 }
2058
2066 public override async Task DeferredConfiguration(HttpServer WebServer)
2067 {
2068 if (this.deferredTest)
2069 {
2070 this.deferredTest = false;
2071 await this.Test(null, true);
2072 }
2073 }
2074 }
2075}
const string DefaultContentType
text/plain
Definition: BinaryCodec.cs:24
Contains information about a response to a content request.
bool HasError
If an error occurred.
object Decoded
Decoded object.
Plain text encoder/decoder.
const string DefaultContentType
text/plain
Helps with common XML-related tasks.
Definition: XML.cs:21
static string HtmlValueEncode(string s)
Differs from Encode(String), in that it does not encode the aposotrophe or the quote.
Definition: XML.cs:211
Static class managing the application event log. Applications and services log events on this static ...
Definition: Log.cs:14
static void Exception(Exception Exception, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, params KeyValuePair< string, object >[] Tags)
Logs an exception. Event type will be determined by the severity of the exception.
Definition: Log.cs:1657
The ClientEvents class allows applications to push information asynchronously to web clients connecte...
Definition: ClientEvents.cs:51
static Task< int > PushEvent(string[] TabIDs, string Type, object Data)
Puses an event to a set of Tabs, given their Tab IDs.
Static class managing the runtime environment of the IoT Gateway.
Definition: Gateway.cs:147
static HttpServer HttpServer
HTTP Server
Definition: Gateway.cs:4118
static IUser AssertUserAuthenticated(HttpRequest Request, string Privilege)
Makes sure a request is being made from a session with a successful user login.
Definition: Gateway.cs:3868
static byte[] NextBytes(int NrBytes)
Generates an array of random bytes.
Definition: Gateway.cs:4335
static int[] GetConfigPorts(string Protocol)
Gets the port numbers defined for a given protocol in the configuration file.
Definition: Gateway.cs:3198
Represents an alternative field in a legal identity.
string Value
Alternative field Value.
string Key
Alternative field name.
string UpdateIpScript
Script to use to update the current IP Address.
AlternativeField[] LocalizedDescriptions
Localized descriptions of domain
static DomainConfiguration Instance
Current instance of configuration.
string Password
Password for PFX file, if any.
const string GATEWAY_DOMAIN_NAME
Main Domain Name of the gateway, if defined. If not provided, the gateway will not use a domain name.
string HumanReadableDescription
Human-readable description of domain
async Task< string > Test(string DomainName, bool EnvironmentSetup, EventHandlerAsync< string > Status)
Checks if the domain points to the server.
string[] EncryptedProperties
Array of properties that are encrypted.
string AcmeDirectory
If a custom Certificate Authority is to be used, this property holds the URL to their ACME directory.
override Task ConfigureSystem()
Is called during startup to configure the system.
async Task< string > AddDomain(string Name, EventHandlerAsync< string > Status)
Adds a domain to the list of domains supported by the gateway.
override void SetStaticInstance(ISystemConfiguration Configuration)
Sets the static instance of the configuration.
string HumanReadableDescriptionLanguage
Language of HumanReadableDescription.
override string ConfigPrivilege
Minimum required privilege for a user to be allowed to change the configuration defined by the class.
string HumanReadableName
Human-readable name of domain
bool CustomCA
If a custom Certificate Authority is to be used
override Task UnregisterSetup(HttpServer WebServer)
Unregisters the setup object.
AlternativeField[] LocalizedNames
Localized names of domain
int DynDnsInterval
Interval (in seconds) for checking if the IP address has changed.
string[] AlternativeDomains
Alternative domain names
override async Task DeferredConfiguration(HttpServer WebServer)
Performs a deferred configuration. Deferred configurations are such that could not be performed durin...
bool IsDomainRegistered(string Name)
Checks if a domain name is registered.
string CheckIpScript
Script to use to evaluate the current IP Address.
async Task< string > CheckDynamicIp(string DomainName, bool EnvironmentSetup, EventHandlerAsync< string > Status)
Checks if Dynamic IP configuration is correct
byte[] PFX
PFX container for certificate and private key, if available.
override Task< bool > SimplifiedConfiguration()
Simplified configuration by configuring simple default values.
override async Task< bool > EnvironmentConfiguration()
Environment configuration by configuring values available in environment variables.
bool AcceptToS
If the CA Terms of Service has been accepted.
override int Priority
Priority of the setting. Configurations are sorted in ascending order.
async Task< string > RemoveDomain(string Name, EventHandlerAsync< string > Status)
Removes a domain from the list of domains supported by the gateway.
bool UseDomainName
If the server uses a domain name.
const string GATEWAY_DYNDNS_UPDATE
Script to use to update the current public IP address of the gateway in the Dynamic DNS service.
string DynDnsAccount
Account Name for the Dynamic DNS service
override Task InitSetup(HttpServer WebServer)
Initializes the setup object.
bool DynamicDns
If the server uses a dynamic DNS service.
bool UseEncryption
If the server uses server-side encryption.
override Task< string > Title(Language Language)
Gets a title for the system configuration.
string DynDnsPassword
Password for the Dynamic DNS service
const string GATEWAY_DOMAIN_ALT
Comma-separated list of alternative domain names for the gateway, if defined.
bool HasToS
If the CA has a Terms of Service.
string HumanReadableNameLanguage
Language of HumanReadableName.
const string GATEWAY_DYNDNS_CHECK
Script to use to check the current public IP address of the gateway.
bool HasCertificate
If the configuration has a certificate.
void LogEnvironmentError(string EnvironmentVariable, object Value)
Logs an error to the event log, telling the operator an environment variable value contains an error.
Abstract base class for multi-step system configurations.
The request could not be understood by the server due to malformed syntax. The client SHOULD NOT repe...
Represents an HTTP request.
Definition: HttpRequest.cs:22
HttpRequestHeader Header
Request header.
Definition: HttpRequest.cs:182
bool HasData
If the request has data.
Definition: HttpRequest.cs:113
string SubPath
Sub-path. If a resource is found handling the request, this property contains the trailing sub-path o...
Definition: HttpRequest.cs:194
async Task< ContentResponse > DecodeDataAsync()
Decodes data sent in request.
Definition: HttpRequest.cs:139
Base class for all HTTP resources.
Definition: HttpResource.cs:23
Represets a response of an HTTP client request.
Definition: HttpResponse.cs:23
async Task SendResponse()
Sends the response back to the client. If the resource is synchronous, there's no need to call this m...
Task Write(byte[] Data)
Returns binary data in the response.
Implements an HTTP server.
Definition: HttpServer.cs:41
static SessionVariables CreateSessionVariables()
Creates a new collection of variables, that contains access to the global set of variables.
Definition: HttpServer.cs:2130
HttpResource Register(HttpResource Resource)
Registers a resource with the server.
Definition: HttpServer.cs:1568
bool Unregister(HttpResource Resource)
Unregisters a resource from the server.
Definition: HttpServer.cs:1743
The server has not found anything matching the Request-URI. No indication is given of whether the con...
Static interface for database persistence. In order to work, a database provider has to be assigned t...
Definition: Database.cs:21
static async Task Update(object Object)
Updates an object in the database.
Definition: Database.cs:1211
Contains information about a language.
Definition: Language.cs:17
Task< string > GetStringAsync(Type Type, int Id, string Default)
Gets the string value of a string ID. If no such string exists, a string is created with the default ...
Definition: Language.cs:209
Static class managing persistent settings.
static async Task< string > GetAsync(string Key, string DefaultValue)
Gets a string-valued setting.
static async Task< bool > SetAsync(string Key, string Value)
Sets a string-valued setting.
Class managing a script expression.
Definition: Expression.cs:41
Collection of variables.
Definition: Variables.cs:25
The request specified an account that does not exist
Represents an ACME account.
Definition: AcmeAccount.cs:34
Task< AcmeAccount > NewKey()
Creates a new key for the account.
Definition: AcmeAccount.cs:149
Task< AcmeOrder > OrderCertificate(string[] Domains, DateTime? NotBefore, DateTime? NotAfter)
Orders certificate.
Definition: AcmeAccount.cs:161
string[] Contact
Optional array of URLs that the server can use to contact the client for issues related to this accou...
Definition: AcmeAccount.cs:98
DateTime? CreatedAt
Date and time of creation, if available.
Definition: AcmeAccount.cs:124
Task< AcmeAccount > Update(string[] Contact)
Updates the account.
Definition: AcmeAccount.cs:131
string InitialIp
Initial IP address.
Definition: AcmeAccount.cs:119
AcmeAccountStatus Status
The status of this account.
Definition: AcmeAccount.cs:103
Represents an ACME authorization.
Task< AcmeAuthorization > Poll()
Gets the current state of the order.
string Value
The identifier itself.
AcmeChallenge[] Challenges
For pending authorizations, the challenges that the client can fulfill in order to prove possession o...
AcmeAuthorizationStatus Status
The status of this authorization.
Base class of all ACME challenges.
AcmeChallengeStatus Status
The status of this challenge.
Task< AcmeChallenge > AcknowledgeChallenge()
Acknowledges the challenge.
Implements an ACME client for the generation of certificates using ACME-compliant certificate servers...
Definition: AcmeClient.cs:24
Represents an ACME directory.
Uri TermsOfService
URL to terms of service.
bool ExternalAccountRequired
If an external account is required.
Represents an ACME HTTP challenge.
Represents an ACME order.
Definition: AcmeOrder.cs:50
AcmeOrderStatus Status
The status of this order.
Definition: AcmeOrder.cs:156
Uri Certificate
A URL for the certificate that has been issued in response to this order.
Definition: AcmeOrder.cs:181
async Task< AcmeAuthorization[]> GetAuthorizations()
Gets current authorization objects.
Definition: AcmeOrder.cs:201
Task< X509Certificate2[]> DownloadCertificate()
Downloads the certificate.
Definition: AcmeOrder.cs:243
Task< AcmeOrder > FinalizeOrder(CertificateRequest CertificateRequest)
Finalize order.
Definition: AcmeOrder.cs:234
Contains methods for simple hash calculations.
Definition: Hashes.cs:57
static string BinaryToString(byte[] Data)
Converts an array of bytes to a string with their hexadecimal representations (in lower case).
Definition: Hashes.cs:63
Encodes data using the Distinguished Encoding Rules (DER), as defined in X.690
Definition: DerEncoder.cs:40
byte[] ToArray()
Converts the generated output to a byte arary.
Definition: DerEncoder.cs:73
RSA with SHA-256 signatures
Definition: RsaSha256.cs:12
Abstract base class for signature algorithms
abstract void ExportPrivateKey(DerEncoder Output)
Exports the private key using DER.
Interface for system configurations. The gateway will scan all module for system configuration classe...
Interface for objects containing encrypted properties. Mark the properties that are encrypted with th...
Definition: ImplTypes.g.cs:58
class Names(Vector NamesVector)
Contains a collection of distinguished names.
Definition: Names.cs:9
delegate string ToString(IElement Element)
Delegate for callback methods that convert an element value to a string.
AcmeOrderStatus
ACME Order status enumeration
Definition: AcmeOrder.cs:15
AcmeAuthorizationStatus
ACME Authorization status enumeration