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;
2using System.Collections.Generic;
3using System.Diagnostics;
4using System.IO;
5using System.Net;
6using System.Net.Http;
7using System.Security.Cryptography;
8using System.Security.Cryptography.X509Certificates;
9using System.Text;
10using System.Threading.Tasks;
11using Waher.Content;
15using Waher.Events;
22using Waher.Script;
23using Waher.Security;
26
28{
33 {
34 private static DomainConfiguration instance = null;
35
36 private HttpResource testDomainNames = null;
37 private HttpResource testDomainName = null;
38 private HttpResource testCA = null;
39 private HttpResource acmeChallenge = null;
40 private HttpResource saveNames = null;
41 private HttpResource saveDescriptions = null;
42
43 private AlternativeField[] localizedNames = null;
44 private AlternativeField[] localizedDescriptions = null;
45 private string[] alternativeDomains = null;
46 private byte[] certificate = null;
47 private byte[] privateKey = null;
48 private byte[] pfx = null;
49 private string humanReadableName = string.Empty;
50 private string humanReadableNameLanguage = string.Empty;
51 private string humanReadableDescription = string.Empty;
52 private string humanReadableDescriptionLanguage = string.Empty;
53 private string domain = string.Empty;
54 private string acmeDirectory = string.Empty;
55 private string contactEMail = string.Empty;
56 private string urlToS = string.Empty;
57 private string password = string.Empty;
58 private string openSslPath = string.Empty;
59 private string dynDnsTemplate = string.Empty;
60 private string checkIpScript = string.Empty;
61 private string updateIpScript = string.Empty;
62 private string dynDnsAccount = string.Empty;
63 private string dynDnsPassword = string.Empty;
64 private int dynDnsInterval = 300;
65 private bool useDomainName = false;
66 private bool dynamicDns = false;
67 private bool useEncryption = true;
68 private bool customCA = false;
69 private bool acceptToS = 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 [DefaultValueStringEmpty]
224 public string Password
225 {
226 get => this.password;
227 set => this.password = value;
228 }
229
233 [DefaultValueStringEmpty]
234 public string OpenSslPath
235 {
236 get => this.openSslPath;
237 set => this.openSslPath = value;
238 }
239
243 [DefaultValueStringEmpty]
244 public string DynDnsTemplate
245 {
246 get => this.dynDnsTemplate;
247 set => this.dynDnsTemplate = value;
248 }
249
253 [DefaultValueStringEmpty]
254 public string CheckIpScript
255 {
256 get => this.checkIpScript;
257 set => this.checkIpScript = value;
258 }
259
263 [DefaultValueStringEmpty]
264 public string UpdateIpScript
265 {
266 get => this.updateIpScript;
267 set => this.updateIpScript = value;
268 }
269
273 [DefaultValueStringEmpty]
274 public string DynDnsAccount
275 {
276 get => this.dynDnsAccount;
277 set => this.dynDnsAccount = value;
278 }
279
283 [DefaultValue(300)]
284 public int DynDnsInterval
285 {
286 get => this.dynDnsInterval;
287 set => this.dynDnsInterval = value;
288 }
289
293 [DefaultValueStringEmpty]
294 public string DynDnsPassword
295 {
296 get => this.dynDnsPassword;
297 set => this.dynDnsPassword = value;
298 }
299
303 [DefaultValueStringEmpty]
304 public string HumanReadableName
305 {
306 get => this.humanReadableName;
307 set => this.humanReadableName = value;
308 }
309
313 [DefaultValueStringEmpty]
315 {
316 get => this.humanReadableNameLanguage;
317 set => this.humanReadableNameLanguage = value;
318 }
319
323 [DefaultValueStringEmpty]
325 {
326 get => this.humanReadableDescription;
327 set => this.humanReadableDescription = value;
328 }
329
333 [DefaultValueStringEmpty]
335 {
336 get => this.humanReadableDescriptionLanguage;
337 set => this.humanReadableDescriptionLanguage = value;
338 }
339
343 [DefaultValueNull]
345 {
346 get => this.localizedNames;
347 set => this.localizedNames = value;
348 }
349
353 [DefaultValueNull]
355 {
356 get => this.localizedDescriptions;
357 set => this.localizedDescriptions = value;
358 }
359
363 public bool HasToS => !string.IsNullOrEmpty(this.urlToS);
364
368 public override string Resource => "/Settings/Domain.md";
369
373 public override int Priority => 200;
374
380 public override Task<string> Title(Language Language)
381 {
382 return Language.GetStringAsync(typeof(Gateway), 3, "Domain");
383 }
384
388 public override Task ConfigureSystem()
389 {
390 return Gateway.ConfigureDomain(this);
391 }
392
397 public override void SetStaticInstance(ISystemConfiguration Configuration)
398 {
399 instance = Configuration as DomainConfiguration;
400 }
401
406 public override Task InitSetup(HttpServer WebServer)
407 {
408 this.testDomainNames = WebServer.Register("/Settings/TestDomainNames", null, this.TestDomainNames, true, false, true);
409 this.testDomainName = WebServer.Register("/Settings/TestDomainName", this.TestDomainName, true, false, true);
410 this.testCA = WebServer.Register("/Settings/TestCA", null, this.TestCA, true, false, true);
411 this.acmeChallenge = WebServer.Register("/.well-known/acme-challenge", this.AcmeChallenge, true, true, true);
412 this.saveNames = WebServer.Register("/Settings/SaveNames", null, this.SaveNames, true, false, true);
413 this.saveDescriptions = WebServer.Register("/Settings/SaveDescriptions", null, this.SaveDescriptions, true, false, true);
414
415 return base.InitSetup(WebServer);
416 }
417
422 public override Task UnregisterSetup(HttpServer WebServer)
423 {
424 WebServer.Unregister(this.testDomainNames);
425 WebServer.Unregister(this.testDomainName);
426 WebServer.Unregister(this.testCA);
427 WebServer.Unregister(this.acmeChallenge);
428 WebServer.Unregister(this.saveNames);
429 WebServer.Unregister(this.saveDescriptions);
430
431 return base.UnregisterSetup(WebServer);
432 }
433
437 protected override string ConfigPrivilege => "Admin.Communication.Domain";
438
439 private async Task TestDomainNames(HttpRequest Request, HttpResponse Response)
440 {
441 Gateway.AssertUserAuthenticated(Request, this.ConfigPrivilege);
442
443 if (!Request.HasData)
444 throw new BadRequestException();
445
446 object Obj = await Request.DecodeDataAsync();
447 if (!(Obj is Dictionary<string, object> Parameters))
448 throw new BadRequestException();
449
450 if (!Parameters.TryGetValue("domainName", out Obj) || !(Obj is string DomainName) ||
451 !Parameters.TryGetValue("dynamicDns", out Obj) || !(Obj is bool DynamicDns) ||
452 !Parameters.TryGetValue("dynDnsTemplate", out Obj) || !(Obj is string DynDnsTemplate) ||
453 !Parameters.TryGetValue("checkIpScript", out Obj) || !(Obj is string CheckIpScript) ||
454 !Parameters.TryGetValue("updateIpScript", out Obj) || !(Obj is string UpdateIpScript) ||
455 !Parameters.TryGetValue("dynDnsAccount", out Obj) || !(Obj is string DynDnsAccount) ||
456 !Parameters.TryGetValue("dynDnsPassword", out Obj) || !(Obj is string DynDnsPassword) ||
457 !Parameters.TryGetValue("dynDnsInterval", out Obj) || !(Obj is int DynDnsInterval))
458 {
459 throw new BadRequestException();
460 }
461
462 if (string.Compare(DomainName, "localhost", true) == 0)
463 throw new BadRequestException("localhost is not a valid domain name.");
464
465 List<string> AlternativeNames = new List<string>();
466 int Index = 0;
467
468 while (Parameters.TryGetValue("altDomainName" + Index.ToString(), out Obj) && Obj is string AltDomainName && !string.IsNullOrEmpty(AltDomainName))
469 {
470 if (string.Compare(AltDomainName, "localhost", true) == 0)
471 throw new BadRequestException("localhost is not a valid domain name.");
472
473 AlternativeNames.Add(AltDomainName);
474 Index++;
475 }
476
477 if (Parameters.TryGetValue("altDomainName", out Obj) && Obj is string AltDomainName2 && !string.IsNullOrEmpty(AltDomainName2))
478 {
479 if (string.Compare(AltDomainName2, "localhost", true) == 0)
480 throw new BadRequestException("localhost is not a valid domain name.");
481
482 AlternativeNames.Add(AltDomainName2);
483 }
484
485 string TabID = Request.Header["X-TabID"];
486 if (string.IsNullOrEmpty(TabID))
487 throw new BadRequestException();
488
489 this.dynamicDns = DynamicDns;
490 this.dynDnsTemplate = DynDnsTemplate;
491 this.checkIpScript = CheckIpScript;
492 this.updateIpScript = UpdateIpScript;
493 this.dynDnsAccount = DynDnsAccount;
494 this.dynDnsPassword = DynDnsPassword;
495 this.dynDnsInterval = DynDnsInterval;
496 this.domain = DomainName;
497 this.alternativeDomains = AlternativeNames.Count == 0 ? null : AlternativeNames.ToArray();
498 this.useDomainName = true;
499
500 Response.StatusCode = 200;
501
502 Task _ = Task.Run(async () => await this.Test(TabID, false));
503 }
504
505 private Task TestDomainName(HttpRequest Request, HttpResponse Response)
506 {
507 Response.StatusCode = 200;
508 Response.ContentType = PlainTextCodec.DefaultContentType;
509 return Response.Write(this.token);
510 }
511
512 private async Task<bool> Test(string TabID, bool EnvironmentSetup)
513 {
514 try
515 {
516 if (!string.IsNullOrEmpty(this.domain))
517 {
518 if (!await this.Test(TabID, EnvironmentSetup, this.domain))
519 {
520 if (string.IsNullOrEmpty(TabID))
521 this.LogEnvironmentError("Domain name not valid.", GATEWAY_DOMAIN_NAME, this.domain);
522 else
523 await ClientEvents.PushEvent(new string[] { TabID }, "NameNotValid", this.domain, false, "User");
524 return false;
525 }
526 }
527
528 if (!(this.alternativeDomains is null))
529 {
530 foreach (string AltDomainName in this.alternativeDomains)
531 {
532 if (!await this.Test(TabID, EnvironmentSetup, AltDomainName))
533 {
534 if (string.IsNullOrEmpty(TabID))
535 this.LogEnvironmentError("Alternative domain name not valid.", GATEWAY_DOMAIN_ALT, AltDomainName);
536 else
537 await ClientEvents.PushEvent(new string[] { TabID }, "NameNotValid", AltDomainName, false, "User");
538 return false;
539 }
540 }
541 }
542
543 if (this.Step < 1)
544 this.Step = 1;
545
546 this.Updated = DateTime.Now;
547 await Database.Update(this);
548
549 if (!string.IsNullOrEmpty(TabID))
550 await ClientEvents.PushEvent(new string[] { TabID }, "NamesOK", string.Empty, false, "User");
551
552 return true;
553 }
554 catch (Exception ex)
555 {
556 if (string.IsNullOrEmpty(TabID))
557 Log.Exception(ex);
558 else
559 await ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", ex.Message, false, "User");
560
561 return false;
562 }
563 }
564
565 internal Task<bool> CheckDynamicIp()
566 {
567 return this.CheckDynamicIp(null, false);
568 }
569
570 internal Task<bool> CheckDynamicIp(bool EnvironmentSetup)
571 {
572 return this.CheckDynamicIp(null, EnvironmentSetup);
573 }
574
575 private async Task<bool> CheckDynamicIp(string TabID, bool EnvironmentSetup)
576 {
577 try
578 {
579 if (!this.useDomainName || !this.dynamicDns)
580 return true;
581
582 bool Result = true;
583
584 if (!await this.CheckDynamicIp(TabID, this.domain, EnvironmentSetup))
585 Result = false;
586
587 foreach (string AlternativeDomain in this.alternativeDomains)
588 {
589 if (!await this.CheckDynamicIp(TabID, AlternativeDomain, EnvironmentSetup))
590 Result = false;
591 }
592
593 return Result;
594 }
595 catch (Exception ex)
596 {
597 Log.Exception(ex);
598 return false;
599 }
600 }
601
602 internal async Task<bool> CheckDynamicIp(string TabID, string DomainName, bool EnvironmentSetup)
603 {
604 string Msg = await this.CheckDynamicIp(DomainName, EnvironmentSetup, async (_, Status) =>
605 {
606 if (!string.IsNullOrEmpty(TabID))
607 await ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", Status, false, "User");
608 });
609
610 if (!string.IsNullOrEmpty(Msg))
611 {
612 if (!string.IsNullOrEmpty(TabID))
613 await ClientEvents.PushEvent(new string[] { TabID }, "CertificateError", Msg, false, "User");
614
615 return false;
616 }
617
618 return true;
619 }
620
628 public async Task<string> CheckDynamicIp(string DomainName, bool EnvironmentSetup, EventHandlerAsync<string> Status)
629 {
630 if (!this.dynamicDns)
631 return null;
632
635
636 try
637 {
638 CheckIpScript = new Expression(this.checkIpScript);
639 }
640 catch (Exception ex)
641 {
642 string Msg = "Unable to parse script checking current IP Address: " + ex.Message;
643
644 if (EnvironmentSetup)
645 this.LogEnvironmentError(Msg, GATEWAY_DYNDNS_CHECK, this.checkIpScript);
646
647 return Msg;
648 }
649
650 try
651 {
652 UpdateIpScript = new Expression(this.updateIpScript);
653 }
654 catch (Exception ex)
655 {
656 string Msg = "Unable to parse script updating the dynamic DNS server: " + ex.Message;
657
658 if (EnvironmentSetup)
659 this.LogEnvironmentError(Msg, GATEWAY_DYNDNS_UPDATE, this.updateIpScript);
660
661 return Msg;
662 }
663
664 await Status.Raise(this, "Checking current IP Address.");
665
667 object Result;
668
669 try
670 {
671 Result = await CheckIpScript.EvaluateAsync(Variables);
672 }
673 catch (Exception ex)
674 {
675 string Msg = "Unable to get 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 if (!(Result is string CurrentIP) || !IPAddress.TryParse(CurrentIP, out IPAddress _))
684 {
685 string Msg = "Unable to get current IP Address. Unexpected response.";
686
687 if (EnvironmentSetup)
688 this.LogEnvironmentError(Msg, GATEWAY_DYNDNS_CHECK, this.checkIpScript);
689
690 return Msg;
691 }
692
693 await Status.Raise(this, "Current IP Address: " + CurrentIP);
694
695 string LastIP = await RuntimeSettings.GetAsync("Last.IP." + DomainName, string.Empty);
696
697 if (LastIP == CurrentIP)
698 await Status.Raise(this, "IP Address has not changed for " + DomainName + ".");
699 else
700 {
701 try
702 {
703 await Status.Raise(this, "Updating IP address for " + DomainName + " to " + CurrentIP + ".");
704
705 Variables["Account"] = this.dynDnsAccount;
706 Variables["Password"] = this.dynDnsPassword;
707 Variables["IP"] = CurrentIP;
708 Variables["Domain"] = DomainName;
709
710 Result = await UpdateIpScript.EvaluateAsync(Variables);
711
712 await RuntimeSettings.SetAsync("Last.IP." + DomainName, CurrentIP);
713 }
714 catch (Exception ex)
715 {
716 string Msg = "Unable to register new dynamic IP Address: " + ex.Message;
717
718 if (EnvironmentSetup)
719 this.LogEnvironmentError(Msg, GATEWAY_DYNDNS_UPDATE, this.updateIpScript);
720
721 return Msg;
722 }
723 }
724
725 return null;
726 }
727
728 private async Task<bool> Test(string TabID, bool EnvironmentSetup, string DomainName)
729 {
730 string Msg = await this.Test(DomainName, EnvironmentSetup, async (_, Status) =>
731 {
732 if (!string.IsNullOrEmpty(TabID))
733 await ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", Status, false, "User");
734 });
735
736 if (!string.IsNullOrEmpty(Msg))
737 {
738 if (!string.IsNullOrEmpty(TabID))
739 await ClientEvents.PushEvent(new string[] { TabID }, "CertificateError", Msg, false, "User");
740
741 return false;
742 }
743
744 return true;
745 }
746
754 public async Task<string> Test(string DomainName, bool EnvironmentSetup, EventHandlerAsync<string> Status)
755 {
756 await Status.Raise(this, "Testing " + DomainName + "...");
757
758 string Msg = await this.CheckDynamicIp(DomainName, EnvironmentSetup, Status);
759 if (!string.IsNullOrEmpty(Msg))
760 return Msg;
761
762 this.token = Hashes.BinaryToString(Gateway.NextBytes(32));
763
764 using (HttpClient HttpClient = new HttpClient()
765 {
766 Timeout = TimeSpan.FromMilliseconds(10000)
767 })
768 {
769 try
770 {
771 StringBuilder Url = new StringBuilder();
772 int[] HttpPorts = Gateway.GetConfigPorts("HTTP");
773
774 Url.Append("http://");
775 Url.Append(DomainName);
776
777 if (Array.IndexOf(HttpPorts, 80) < 0 && HttpPorts.Length > 0)
778 {
779 Url.Append(':');
780 Url.Append(HttpPorts[0].ToString());
781 }
782
783 Url.Append("/Settings/TestDomainName");
784
785 HttpResponseMessage Response = await HttpClient.GetAsync("http://" + DomainName + "/Settings/TestDomainName");
786 if (!Response.IsSuccessStatusCode)
787 {
788 return "Domain name does not point to this machine.";
789 }
790
791 byte[] Bin = await Response.Content.ReadAsByteArrayAsync();
792 string Token = System.Text.Encoding.ASCII.GetString(Bin);
793
794 if (Token != this.token)
795 return "Unexpected response returned. Domain name does not point to this machine.";
796 }
797 catch (TimeoutException)
798 {
799 return "Time-out. Check that the domain name points to this machine.";
800 }
801 catch (Exception ex)
802 {
803 return "Unable to validate domain name: " + ex.Message;
804 }
805 }
806
807 await Status.Raise(this, "Domain name valid.");
808
809 return null;
810 }
811
812 private async Task TestCA(HttpRequest Request, HttpResponse Response)
813 {
814 Gateway.AssertUserAuthenticated(Request, this.ConfigPrivilege);
815
816 if (!Request.HasData)
817 throw new BadRequestException();
818
819 object Obj = await Request.DecodeDataAsync();
820 if (!(Obj is Dictionary<string, object> Parameters))
821 throw new BadRequestException();
822
823 if (!Parameters.TryGetValue("useEncryption", out Obj) || !(Obj is bool UseEncryption))
824 throw new BadRequestException();
825
826 if (!Parameters.TryGetValue("customCA", out Obj) || !(Obj is bool CustomCA))
827 throw new BadRequestException();
828
829 if (!Parameters.TryGetValue("acmeDirectory", out Obj) || !(Obj is string AcmeDirectory))
830 throw new BadRequestException();
831
832 if (!Parameters.TryGetValue("contactEMail", out Obj) || !(Obj is string ContactEMail))
833 throw new BadRequestException();
834
835 if (!Parameters.TryGetValue("acceptToS", out Obj) || !(Obj is bool AcceptToS))
836 throw new BadRequestException();
837
838 if (!Parameters.TryGetValue("domainName", out Obj) || !(Obj is string DomainName) ||
839 !Parameters.TryGetValue("dynamicDns", out Obj) || !(Obj is bool DynamicDns) ||
840 !Parameters.TryGetValue("dynDnsTemplate", out Obj) || !(Obj is string DynDnsTemplate) ||
841 !Parameters.TryGetValue("checkIpScript", out Obj) || !(Obj is string CheckIpScript) ||
842 !Parameters.TryGetValue("updateIpScript", out Obj) || !(Obj is string UpdateIpScript) ||
843 !Parameters.TryGetValue("dynDnsAccount", out Obj) || !(Obj is string DynDnsAccount) ||
844 !Parameters.TryGetValue("dynDnsPassword", out Obj) || !(Obj is string DynDnsPassword) ||
845 !Parameters.TryGetValue("dynDnsInterval", out Obj) || !(Obj is int DynDnsInterval))
846 throw new BadRequestException();
847
848 List<string> AlternativeNames = new List<string>();
849 int Index = 0;
850
851 while (Parameters.TryGetValue("altDomainName" + Index.ToString(), out Obj) && Obj is string AltDomainName && !string.IsNullOrEmpty(AltDomainName))
852 {
853 AlternativeNames.Add(AltDomainName);
854 Index++;
855 }
856
857 if (Parameters.TryGetValue("altDomainName", out Obj) && Obj is string AltDomainName2 && !string.IsNullOrEmpty(AltDomainName2))
858 AlternativeNames.Add(AltDomainName2);
859
860 string TabID = Request.Header["X-TabID"];
861 if (string.IsNullOrEmpty(TabID))
862 throw new BadRequestException();
863
864 this.dynamicDns = DynamicDns;
865 this.dynDnsTemplate = DynDnsTemplate;
866 this.checkIpScript = CheckIpScript;
867 this.updateIpScript = UpdateIpScript;
868 this.dynDnsAccount = DynDnsAccount;
869 this.dynDnsPassword = DynDnsPassword;
870 this.dynDnsInterval = DynDnsInterval;
871 this.domain = DomainName;
872 this.alternativeDomains = AlternativeNames.Count == 0 ? null : AlternativeNames.ToArray();
873 this.useDomainName = true;
874 this.useEncryption = UseEncryption;
875 this.customCA = CustomCA;
876 this.acmeDirectory = AcmeDirectory;
877 this.contactEMail = ContactEMail;
878 this.acceptToS = AcceptToS;
879
880 Response.StatusCode = 200;
881
882 if (!this.inProgress)
883 {
884 this.inProgress = true;
885 Task _ = Task.Run(async () =>
886 {
887 try
888 {
889 await this.CreateCertificate(TabID, false);
890 }
891 catch (Exception ex)
892 {
893 Log.Exception(ex);
894 }
895 });
896 }
897 }
898
899 private bool inProgress = false;
900
907 public async Task<string> AddDomain(string Name, EventHandlerAsync<string> Status)
908 {
909 if (this.IsDomainRegistered(Name))
910 return "Domain already registered.";
911
912 string Msg = await this.Test(Name, false, Status);
913 if (!string.IsNullOrEmpty(Msg))
914 return Msg;
915
916 string DomainBak = this.domain;
917 bool UseDomainBak = this.useDomainName;
918 string[] AlternativeBak = this.alternativeDomains;
919
920 if (string.IsNullOrEmpty(this.domain))
921 {
922 this.domain = Name;
923 this.useDomainName = true;
924 }
925 else if (this.alternativeDomains is null)
926 this.alternativeDomains = new string[] { Name };
927 else
928 {
929 int c = this.alternativeDomains.Length;
930 string[] NewArray = new string[c + 1];
931 this.alternativeDomains.CopyTo(NewArray, 0);
932 NewArray[c] = Name;
933 this.alternativeDomains = NewArray;
934 }
935
936 Msg = await this.CreateCertificate(false, Status, (_, __) => Task.CompletedTask);
937 if (!string.IsNullOrEmpty(Msg))
938 {
939 this.domain = DomainBak;
940 this.useDomainName = UseDomainBak;
941 this.alternativeDomains = AlternativeBak;
942
943 return Msg;
944 }
945
946 this.Updated = DateTime.Now;
947 await Database.Update(this);
948
949 return null;
950 }
951
958 public async Task<string> RemoveDomain(string Name, EventHandlerAsync<string> Status)
959 {
960 if (!this.IsDomainRegistered(Name))
961 return "Domain not registered.";
962
963 string DomainBak = this.domain;
964 bool UseDomainBak = this.useDomainName;
965 string[] AlternativeBak = this.alternativeDomains;
966 string[] NewArray;
967
968 if (string.Compare(this.domain, Name, true) == 0)
969 {
970 if ((this.alternativeDomains?.Length ?? 0) == 0)
971 {
972 this.domain = string.Empty;
973 this.useDomainName = false;
974 }
975 else
976 {
977 this.domain = this.alternativeDomains[0];
978
979 int c = this.alternativeDomains.Length;
980
981 if (c == 1)
982 this.alternativeDomains = null;
983 else
984 {
985 NewArray = new string[c - 1];
986 Array.Copy(this.alternativeDomains, 1, NewArray, 0, c - 1);
987 this.alternativeDomains = NewArray;
988 }
989 }
990 }
991 else if (!(this.alternativeDomains is null))
992 {
993 int c = this.alternativeDomains.Length;
994 if (c == 1)
995 this.alternativeDomains = null;
996 else
997 {
998 int i = Array.IndexOf(this.alternativeDomains, Name);
999
1000 NewArray = new string[c - 1];
1001
1002 if (i > 0)
1003 Array.Copy(this.alternativeDomains, 0, NewArray, 0, i);
1004
1005 if (i < c - 1)
1006 Array.Copy(this.alternativeDomains, i + 1, NewArray, i, c - i - 1);
1007
1008 this.alternativeDomains = NewArray;
1009 }
1010 }
1011
1012 if (this.useDomainName)
1013 {
1014 string Msg = await this.CreateCertificate(false, Status, (_, __) => Task.CompletedTask);
1015 if (!string.IsNullOrEmpty(Msg))
1016 {
1017 this.domain = DomainBak;
1018 this.useDomainName = UseDomainBak;
1019 this.alternativeDomains = AlternativeBak;
1020
1021 return Msg;
1022 }
1023 }
1024
1025 this.Updated = DateTime.Now;
1026 await Database.Update(this);
1027
1028 return null;
1029 }
1030
1036 public bool IsDomainRegistered(string Name)
1037 {
1038 if (string.Compare(Name, this.domain, true) == 0)
1039 return true;
1040
1041 if (this.alternativeDomains is null)
1042 return false;
1043
1044 foreach (string Alternative in this.alternativeDomains)
1045 {
1046 if (string.Compare(Name, Alternative, true) == 0)
1047 return true;
1048 }
1049
1050 return false;
1051 }
1052
1053 internal Task<bool> CreateCertificate()
1054 {
1055 return this.CreateCertificate(null, false);
1056 }
1057
1058 private async Task<bool> CreateCertificate(string TabID, bool EnvironmentSetup)
1059 {
1060 try
1061 {
1062 string Msg = await this.CreateCertificate(EnvironmentSetup,
1063 async (_, Status) =>
1064 {
1065 if (!string.IsNullOrEmpty(TabID))
1066 await ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", Status, false, "User");
1067 },
1068 async (_, URL) =>
1069 {
1070 if (!string.IsNullOrEmpty(TabID))
1071 await ClientEvents.PushEvent(new string[] { TabID }, "TermsOfService", URL, false, "User");
1072 });
1073
1074 if (string.IsNullOrEmpty(Msg))
1075 {
1076 if (!string.IsNullOrEmpty(TabID))
1077 await ClientEvents.PushEvent(new string[] { TabID }, "CertificateOk", string.Empty, false, "User");
1078
1079 return true;
1080 }
1081 else
1082 {
1083 if (!string.IsNullOrEmpty(TabID))
1084 await ClientEvents.PushEvent(new string[] { TabID }, "CertificateError", Msg, false, "User");
1085
1086 return false;
1087 }
1088 }
1089 catch (Exception ex)
1090 {
1091 if (!string.IsNullOrEmpty(TabID))
1092 await ClientEvents.PushEvent(new string[] { TabID }, "CertificateError", ex.Message, false, "User");
1093
1094 if (EnvironmentSetup)
1095 this.LogEnvironmentError(ex.Message, GATEWAY_ENCRYPTION, this.useEncryption);
1096
1097 return false;
1098 }
1099 }
1100
1101 internal async Task<string> CreateCertificate(bool EnvironmentSetup, EventHandlerAsync<string> Status,
1102 EventHandlerAsync<string> TermsOfService)
1103 {
1104 try
1105 {
1106 string URL = this.customCA ? this.acmeDirectory : "https://acme-v02.api.letsencrypt.org/directory";
1107 RSAParameters Parameters;
1108 CspParameters CspParams = new CspParameters()
1109 {
1110 Flags = CspProviderFlags.UseMachineKeyStore,
1111 KeyContainerName = "IoTGateway:" + URL
1112 };
1113
1114 try
1115 {
1116 bool Ok;
1117
1118 using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(4096, CspParams))
1119 {
1120 Parameters = RSA.ExportParameters(true);
1121
1122 if (RSA.KeySize < 4096)
1123 {
1124 RSA.PersistKeyInCsp = false;
1125 RSA.Clear();
1126 Ok = false;
1127 }
1128 else
1129 Ok = true;
1130 }
1131
1132 if (!Ok)
1133 {
1134 using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(4096, CspParams))
1135 {
1136 Parameters = RSA.ExportParameters(true);
1137 }
1138 }
1139 }
1140 catch (CryptographicException ex)
1141 {
1142 throw new CryptographicException("Unable to get access to cryptographic key for \"IoTGateway:" + URL +
1143 "\". Was the database created using another user?", ex);
1144 }
1145
1146 using (AcmeClient Client = new AcmeClient(new Uri(URL), Parameters))
1147 {
1148 await Status.Raise(this, "Connecting to directory.");
1149
1150 AcmeDirectory AcmeDirectory = await Client.GetDirectory();
1151
1153 await Status.Raise(this, "An external account is required.");
1154
1155 if (!(AcmeDirectory.TermsOfService is null))
1156 {
1157 URL = AcmeDirectory.TermsOfService.ToString();
1158 await Status.Raise(this, "Terms of service available on: " + URL);
1159 await TermsOfService.Raise(this, URL);
1160
1161 this.urlToS = URL;
1162
1163 if (!this.acceptToS)
1164 {
1165 string Msg = "You need to accept the terms of service.";
1166
1167 if (EnvironmentSetup)
1168 this.LogEnvironmentError(Msg, GATEWAY_ACME_ACCEPT_TOS, this.acceptToS);
1169
1170 return Msg;
1171 }
1172 }
1173
1174 if (!(AcmeDirectory.Website is null))
1175 await Status.Raise(this, "Web site available on: " + AcmeDirectory.Website.ToString());
1176
1177 await Status.Raise(this, "Getting account.");
1178
1179 List<string> Names = new List<string>();
1180
1181 if (!string.IsNullOrEmpty(this.domain))
1182 Names.Add(this.domain);
1183
1184 if (!(this.alternativeDomains is null))
1185 {
1186 foreach (string Name in this.alternativeDomains)
1187 {
1188 if (!Names.Contains(Name))
1189 Names.Add(Name);
1190 }
1191 }
1192 string[] DomainNames = Names.ToArray();
1193
1194 AcmeAccount Account;
1195
1196 try
1197 {
1198 Account = await Client.GetAccount();
1199
1200 await Status.Raise(this, "Account found.");
1201 await Status.Raise(this, "Created: " + Account.CreatedAt.ToString());
1202 await Status.Raise(this, "Initial IP: " + Account.InitialIp);
1203 await Status.Raise(this, "Status: " + Account.Status.ToString());
1204
1205 if (string.IsNullOrEmpty(this.contactEMail))
1206 {
1207 if (!(Account.Contact is null) && Account.Contact.Length != 0)
1208 {
1209 await Status.Raise(this, "Updating contact URIs in account.");
1210 Account = await Account.Update(new string[0]);
1211 await Status.Raise(this, "Account updated.");
1212 }
1213 }
1214 else
1215 {
1216 if (Account.Contact is null || Account.Contact.Length != 1 || Account.Contact[0] != "mailto:" + this.contactEMail)
1217 {
1218 await Status.Raise(this, "Updating contact URIs in account.");
1219 Account = await Account.Update(new string[] { "mailto:" + this.contactEMail });
1220 await Status.Raise(this, "Account updated.");
1221 }
1222 }
1223 }
1225 {
1226 await Status.Raise(this, "Account not found.");
1227 await Status.Raise(this, "Creating account.");
1228
1229 Account = await Client.CreateAccount(string.IsNullOrEmpty(this.contactEMail) ? new string[0] : new string[] { "mailto:" + this.contactEMail },
1230 this.acceptToS);
1231
1232 await Status.Raise(this, "Account created.");
1233 await Status.Raise(this, "Status: " + Account.Status.ToString());
1234 }
1235
1236 await Status.Raise(this, "Generating new key.");
1237 await Account.NewKey();
1238
1239 using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(4096, CspParams))
1240 {
1241 RSA.ImportParameters(Client.ExportAccountKey(true));
1242 }
1243
1244 await Status.Raise(this, "New key generated.");
1245
1246 await Status.Raise(this, "Creating order.");
1247 AcmeOrder Order;
1248
1249 try
1250 {
1251 Order = await Account.OrderCertificate(DomainNames, null, null);
1252 }
1253 catch (AcmeMalformedException) // Not sure why this is necessary. Perhaps because it takes time to propagate the keys correctly on the remote end?
1254 {
1255 await Task.Delay(5000);
1256 await Status.Raise(this, "Retrying.");
1257 Order = await Account.OrderCertificate(DomainNames, null, null);
1258 }
1259
1260 await Status.Raise(this, "Order created.");
1261
1262 AcmeAuthorization[] Authorizations;
1263
1264 await Status.Raise(this, "Getting authorizations.");
1265 try
1266 {
1267 Authorizations = await Order.GetAuthorizations();
1268 }
1269 catch (AcmeMalformedException) // Not sure why this is necessary. Perhaps because it takes time to propagate the keys correctly on the remote end?
1270 {
1271 await Task.Delay(5000);
1272 await Status.Raise(this, "Retrying.");
1273 Authorizations = await Order.GetAuthorizations();
1274 }
1275
1276 foreach (AcmeAuthorization Authorization in Authorizations)
1277 {
1278 await Status.Raise(this, "Processing authorization for " + Authorization.Value);
1279
1280 AcmeChallenge Challenge;
1281 bool Acknowledged = false;
1282 int Index = 1;
1283 int NrChallenges = Authorization.Challenges.Length;
1284
1285 for (Index = 1; Index <= NrChallenges; Index++)
1286 {
1287 Challenge = Authorization.Challenges[Index - 1];
1288
1289 if (Challenge is AcmeHttpChallenge HttpChallenge)
1290 {
1291 this.challenge = "/" + HttpChallenge.Token;
1292 this.token = HttpChallenge.KeyAuthorization;
1293
1294 await Status.Raise(this, "Acknowleding challenge.");
1295
1296 string Msg = await this.CheckDynamicIp(Authorization.Value, EnvironmentSetup, Status);
1297 if (string.IsNullOrEmpty(Msg))
1298 {
1299 Challenge = await HttpChallenge.AcknowledgeChallenge();
1300 await Status.Raise(this, "Challenge acknowledged: " + Challenge.Status.ToString());
1301
1302 Acknowledged = true;
1303 }
1304 else
1305 return Msg;
1306 }
1307 }
1308
1309 if (!Acknowledged)
1310 {
1311 string Msg = "No automated method found to respond to any of the authorization challenges.";
1312
1313 if (EnvironmentSetup)
1314 this.LogEnvironmentError(Msg, GATEWAY_ENCRYPTION, this.useEncryption);
1315
1316 return Msg;
1317 }
1318
1319 AcmeAuthorization Authorization2 = Authorization;
1320
1321 do
1322 {
1323 await Status.Raise(this, "Waiting to poll authorization status.");
1324 await Task.Delay(5000);
1325
1326 await Status.Raise(this, "Polling authorization.");
1327 Authorization2 = await Authorization2.Poll();
1328
1329 await Status.Raise(this, "Authorization polled: " + Authorization2.Status.ToString());
1330 }
1331 while (Authorization2.Status == AcmeAuthorizationStatus.pending);
1332
1333 if (Authorization2.Status != AcmeAuthorizationStatus.valid)
1334 {
1335 switch (Authorization2.Status)
1336 {
1337 case AcmeAuthorizationStatus.deactivated:
1338 throw new Exception("Authorization deactivated.");
1339
1340 case AcmeAuthorizationStatus.expired:
1341 throw new Exception("Authorization expired.");
1342
1343 case AcmeAuthorizationStatus.invalid:
1344 throw new Exception("Authorization invalid.");
1345
1346 case AcmeAuthorizationStatus.revoked:
1347 throw new Exception("Authorization revoked.");
1348
1349 default:
1350 throw new Exception("Authorization not validated.");
1351 }
1352 }
1353 }
1354
1355 using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(4096)) // TODO: Make configurable
1356 {
1357 await Status.Raise(this, "Finalizing order.");
1358
1359 SignatureAlgorithm SignAlg = new RsaSha256(RSA);
1360
1361 Order = await Order.FinalizeOrder(new CertificateRequest(SignAlg)
1362 {
1363 CommonName = this.domain,
1364 SubjectAlternativeNames = DomainNames,
1365 EMailAddress = this.contactEMail
1366 });
1367
1368 await Status.Raise(this, "Order finalized: " + Order.Status.ToString());
1369
1370 if (Order.Status != AcmeOrderStatus.valid)
1371 {
1372 switch (Order.Status)
1373 {
1374 case AcmeOrderStatus.invalid:
1375 throw new Exception("Order invalid.");
1376
1377 default:
1378 throw new Exception("Unable to validate order.");
1379 }
1380 }
1381
1382 if (Order.Certificate is null)
1383 throw new Exception("No certificate URI provided.");
1384
1385 await Status.Raise(this, "Downloading certificate.");
1386
1387 X509Certificate2[] Certificates = await Order.DownloadCertificate();
1388 X509Certificate2 Certificate = Certificates[0];
1389
1390 await Status.Raise(this, "Exporting certificate.");
1391
1392 this.certificate = Certificate.Export(X509ContentType.Cert);
1393 this.privateKey = RSA.ExportCspBlob(true);
1394 this.pfx = null;
1395 this.password = string.Empty;
1396
1397 await Status.Raise(this, "Adding private key.");
1398
1399 try
1400 {
1401 Certificate.PrivateKey = RSA;
1402 }
1403 catch (PlatformNotSupportedException)
1404 {
1405 await Status.Raise(this, "Platform does not support adding of private key.");
1406 await Status.Raise(this, "Searching for OpenSSL on machine.");
1407
1408 string[] Files;
1409 string Password = Hashes.BinaryToString(Gateway.NextBytes(32));
1410 string CertFileName = null;
1411 string CertFileName2 = null;
1412 string KeyFileName = null;
1413
1414 if (string.IsNullOrEmpty(this.openSslPath) || !File.Exists(this.openSslPath))
1415 {
1416 string[] Folders = Gateway.GetFolders(new Environment.SpecialFolder[]
1417 {
1418 Environment.SpecialFolder.ProgramFiles,
1419 Environment.SpecialFolder.ProgramFilesX86
1420 },
1421 Path.DirectorySeparatorChar + "OpenSSL-Win32",
1422 Path.DirectorySeparatorChar + "OpenSSL-Win64");
1423
1424 Files = Gateway.FindFiles(Folders, "openssl.exe", 2, int.MaxValue);
1425 }
1426 else
1427 Files = new string[] { this.openSslPath };
1428
1429 try
1430 {
1431 if (Files.Length == 0)
1432 {
1433 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.";
1434
1435 if (EnvironmentSetup)
1436 this.LogEnvironmentError(Msg, GATEWAY_ENCRYPTION, this.useEncryption);
1437
1438 return Msg;
1439 }
1440 else
1441 {
1442 foreach (string OpenSslFile in Files)
1443 {
1444 if (CertFileName is null)
1445 {
1446 await Status.Raise(this, "Generating temporary certificate file.");
1447
1448 StringBuilder PemOutput = new StringBuilder();
1449 byte[] Bin = Certificate.Export(X509ContentType.Cert);
1450
1451 PemOutput.AppendLine("-----BEGIN CERTIFICATE-----");
1452 PemOutput.AppendLine(Convert.ToBase64String(Bin, Base64FormattingOptions.InsertLineBreaks));
1453 PemOutput.AppendLine("-----END CERTIFICATE-----");
1454
1455 CertFileName = Path.Combine(Gateway.AppDataFolder, "Certificate.pem");
1456 await Resources.WriteAllTextAsync(CertFileName, PemOutput.ToString(), System.Text.Encoding.ASCII);
1457
1458 await Status.Raise(this, "Generating temporary key file.");
1459
1460 DerEncoder KeyOutput = new DerEncoder();
1461 SignAlg.ExportPrivateKey(KeyOutput);
1462
1463 PemOutput.Clear();
1464 PemOutput.AppendLine("-----BEGIN RSA PRIVATE KEY-----");
1465 PemOutput.AppendLine(Convert.ToBase64String(KeyOutput.ToArray(), Base64FormattingOptions.InsertLineBreaks));
1466 PemOutput.AppendLine("-----END RSA PRIVATE KEY-----");
1467
1468 KeyFileName = Path.Combine(Gateway.AppDataFolder, "Certificate.key");
1469
1470 await Resources.WriteAllTextAsync(KeyFileName, PemOutput.ToString(), System.Text.Encoding.ASCII);
1471 }
1472
1473 await Status.Raise(this, "Converting to PFX using " + OpenSslFile);
1474
1475 Process P = new Process()
1476 {
1477 StartInfo = new ProcessStartInfo()
1478 {
1479 FileName = OpenSslFile,
1480 Arguments = "pkcs12 -nodes -export -out Certificate.pfx -inkey Certificate.key -in Certificate.pem -password pass:" + Password,
1481 UseShellExecute = false,
1482 RedirectStandardError = true,
1483 RedirectStandardOutput = true,
1484 WorkingDirectory = Gateway.AppDataFolder,
1485 CreateNoWindow = true,
1486 WindowStyle = ProcessWindowStyle.Hidden
1487 }
1488 };
1489
1490 P.Start();
1491
1492 if (!P.WaitForExit(60000) || P.ExitCode != 0)
1493 {
1494 if (!P.StandardOutput.EndOfStream)
1495 await Status.Raise(this, "Output: " + P.StandardOutput.ReadToEnd());
1496
1497 if (!P.StandardError.EndOfStream)
1498 await Status.Raise(this, "Error: " + P.StandardError.ReadToEnd());
1499
1500 continue;
1501 }
1502
1503 await Status.Raise(this, "Loading PFX.");
1504
1505 CertFileName2 = Path.Combine(Gateway.AppDataFolder, "Certificate.pfx");
1506 this.pfx = await Resources.ReadAllBytesAsync(CertFileName2);
1507 this.password = Password;
1508 this.openSslPath = OpenSslFile;
1509
1510 await Status.Raise(this, "PFX successfully generated using OpenSSL.");
1511 break;
1512 }
1513
1514 if (this.pfx is null)
1515 {
1516 this.openSslPath = string.Empty;
1517
1518 string Msg = "Unable to convert to PFX using OpenSSL.";
1519
1520 if (EnvironmentSetup)
1521 this.LogEnvironmentError(Msg, GATEWAY_ENCRYPTION, this.useEncryption);
1522
1523 return Msg;
1524 }
1525 }
1526 }
1527 finally
1528 {
1529 if (!(CertFileName is null) && File.Exists(CertFileName))
1530 {
1531 await Status.Raise(this, "Deleting temporary certificate file.");
1532 File.Delete(CertFileName);
1533 }
1534
1535 if (!(KeyFileName is null) && File.Exists(KeyFileName))
1536 {
1537 await Status.Raise(this, "Deleting temporary key file.");
1538 File.Delete(KeyFileName);
1539 }
1540
1541 if (!(CertFileName2 is null) && File.Exists(CertFileName2))
1542 {
1543 await Status.Raise(this, "Deleting temporary pfx file.");
1544 File.Delete(CertFileName2);
1545 }
1546 }
1547 }
1548
1549
1550 if (this.Step < 2)
1551 this.Step = 2;
1552
1553 this.Updated = DateTime.Now;
1554 await Database.Update(this);
1555
1556 await Gateway.UpdateCertificate(this);
1557
1558 return null;
1559 }
1560 }
1561 }
1562 catch (Exception ex)
1563 {
1564 Log.Exception(ex);
1565
1566 string Msg = "Unable to create certificate: " + XML.HtmlValueEncode(ex.Message);
1567
1568 if (EnvironmentSetup)
1569 this.LogEnvironmentError(Msg, GATEWAY_ENCRYPTION, this.useEncryption);
1570
1571 return Msg;
1572 }
1573 finally
1574 {
1575 this.inProgress = false;
1576 }
1577 }
1578
1579 private Task AcmeChallenge(HttpRequest Request, HttpResponse Response)
1580 {
1581 if (Request.SubPath != this.challenge)
1582 throw new NotFoundException("ACME Challenge not found.");
1583
1584 Response.StatusCode = 200;
1585 Response.ContentType = BinaryCodec.DefaultContentType;
1586 return Response.Write(System.Text.Encoding.ASCII.GetBytes(this.token));
1587 }
1588
1589 private async Task SaveNames(HttpRequest Request, HttpResponse Response)
1590 {
1591 Gateway.AssertUserAuthenticated(Request, this.ConfigPrivilege);
1592
1593 if (!Request.HasData)
1594 throw new BadRequestException();
1595
1596 object Obj = await Request.DecodeDataAsync();
1597 if (!(Obj is Dictionary<string, object> Parameters))
1598 throw new BadRequestException();
1599
1600 if (!Parameters.TryGetValue("humanReadableName", out Obj) ||
1601 !(Obj is string HumanReadableName) ||
1602 !Parameters.TryGetValue("humanReadableNameLanguage", out Obj) ||
1603 !(Obj is string HumanReadableNameLanguage))
1604 {
1605 throw new BadRequestException();
1606 }
1607
1608 List<AlternativeField> LocalizedNames = new List<AlternativeField>();
1609 int Index = 1;
1610
1611 while (Parameters.TryGetValue("nameLanguage" + Index.ToString(), out Obj) &&
1612 Obj is string NameLanguage &&
1613 Parameters.TryGetValue("nameLocalized" + Index.ToString(), out Obj) &&
1614 Obj is string NameLocalized)
1615 {
1616 if (string.IsNullOrEmpty(NameLanguage))
1617 throw new BadRequestException("Language cannot be empty.");
1618
1619 if (string.IsNullOrEmpty(NameLocalized))
1620 throw new BadRequestException("Localized name cannot be empty.");
1621
1622 LocalizedNames.Add(new AlternativeField(NameLanguage, NameLocalized));
1623 Index++;
1624 }
1625
1626 this.HumanReadableName = HumanReadableName;
1627 this.HumanReadableNameLanguage = HumanReadableNameLanguage;
1628 this.LocalizedNames = LocalizedNames.ToArray();
1629
1630 this.Updated = DateTime.Now;
1631 await Database.Update(this);
1632 }
1633
1634 private async Task SaveDescriptions(HttpRequest Request, HttpResponse Response)
1635 {
1636 Gateway.AssertUserAuthenticated(Request, this.ConfigPrivilege);
1637
1638 if (!Request.HasData)
1639 throw new BadRequestException();
1640
1641 object Obj = await Request.DecodeDataAsync();
1642 if (!(Obj is Dictionary<string, object> Parameters))
1643 throw new BadRequestException();
1644
1645 if (!Parameters.TryGetValue("humanReadableDescription", out Obj) ||
1646 !(Obj is string HumanReadableDescription) ||
1647 !Parameters.TryGetValue("humanReadableDescriptionLanguage", out Obj) ||
1648 !(Obj is string HumanReadableDescriptionLanguage))
1649 {
1650 throw new BadRequestException();
1651 }
1652
1653 List<AlternativeField> LocalizedDescriptions = new List<AlternativeField>();
1654 int Index = 1;
1655
1656 while (Parameters.TryGetValue("descriptionLanguage" + Index.ToString(), out Obj) &&
1657 Obj is string DescriptionLanguage &&
1658 Parameters.TryGetValue("descriptionLocalized" + Index.ToString(), out Obj) &&
1659 Obj is string DescriptionLocalized)
1660 {
1661 if (string.IsNullOrEmpty(DescriptionLanguage))
1662 throw new BadRequestException("Language cannot be empty.");
1663
1664 if (string.IsNullOrEmpty(DescriptionLocalized))
1665 throw new BadRequestException("Localized description cannot be empty.");
1666
1667 LocalizedDescriptions.Add(new AlternativeField(DescriptionLanguage, DescriptionLocalized));
1668 Index++;
1669 }
1670
1671 this.HumanReadableDescription = HumanReadableDescription;
1672 this.HumanReadableDescriptionLanguage = HumanReadableDescriptionLanguage;
1673 this.LocalizedDescriptions = LocalizedDescriptions.ToArray();
1674
1675 this.Updated = DateTime.Now;
1676 await Database.Update(this);
1677 }
1678
1683 public override Task<bool> SimplifiedConfiguration()
1684 {
1685 return Task.FromResult(true);
1686 }
1687
1691 public const string GATEWAY_DOMAIN_USE = nameof(GATEWAY_DOMAIN_USE);
1692
1696 public const string GATEWAY_DOMAIN_NAME = nameof(GATEWAY_DOMAIN_NAME);
1697
1698 // If a Domain Name is configured(<see cref="GATEWAY_DOMAIN"/> variable), the following variables define its operation:
1699
1703 public const string GATEWAY_DOMAIN_ALT = nameof(GATEWAY_DOMAIN_ALT);
1704
1708 public const string GATEWAY_DYNDNS = nameof(GATEWAY_DYNDNS);
1709
1713 public const string GATEWAY_ENCRYPTION = nameof(GATEWAY_ENCRYPTION);
1714
1718 public const string GATEWAY_CA_CUSTOM = nameof(GATEWAY_CA_CUSTOM);
1719
1723 public const string GATEWAY_ACME_EMAIL = nameof(GATEWAY_ACME_EMAIL);
1724
1728 public const string GATEWAY_ACME_ACCEPT_TOS = nameof(GATEWAY_ACME_ACCEPT_TOS);
1729
1733 public const string GATEWAY_HR_NAME = nameof(GATEWAY_HR_NAME);
1734
1738 public const string GATEWAY_HR_NAME_LANG = nameof(GATEWAY_HR_NAME_LANG);
1739
1743 public const string GATEWAY_HR_DESC = nameof(GATEWAY_HR_DESC);
1744
1748 public const string GATEWAY_HR_DESC_LANG = nameof(GATEWAY_HR_DESC_LANG);
1749
1753 public const string GATEWAY_HR_NAME_LOC = nameof(GATEWAY_HR_NAME_LOC);
1754
1758 public const string GATEWAY_HR_NAME_ = nameof(GATEWAY_HR_NAME_);
1759
1763 public const string GATEWAY_HR_DESC_LOC = nameof(GATEWAY_HR_DESC_LOC);
1764
1768 public const string GATEWAY_HR_DESC_ = nameof(GATEWAY_HR_DESC_);
1769
1770 // If Dynamic DNS is configured (<see cref="GATEWAY_DYNDNS"/> variable), the following variables define its operation:
1771
1775 public const string GATEWAY_DYNDNS_TEMPLATE = nameof(GATEWAY_DYNDNS_TEMPLATE);
1776
1780 public const string GATEWAY_DYNDNS_CHECK = nameof(GATEWAY_DYNDNS_CHECK);
1781
1785 public const string GATEWAY_DYNDNS_UPDATE = nameof(GATEWAY_DYNDNS_UPDATE);
1786
1790 public const string GATEWAY_DYNDNS_ACCOUNT = nameof(GATEWAY_DYNDNS_ACCOUNT);
1791
1795 public const string GATEWAY_DYNDNS_PASSWORD = nameof(GATEWAY_DYNDNS_PASSWORD);
1796
1800 public const string GATEWAY_DYNDNS_INTERVAL = nameof(GATEWAY_DYNDNS_INTERVAL);
1801
1802 // If a Custom Certificate Authority is configured(<see cref="GATEWAY_CA_CUSTOM"/> variable), the following variables define its operation:
1803
1807 public const string GATEWAY_ACME_DIRECTORY = nameof(GATEWAY_ACME_DIRECTORY);
1808
1813 public override async Task<bool> EnvironmentConfiguration()
1814 {
1815 if (!this.TryGetEnvironmentVariable(GATEWAY_DOMAIN_USE, false, out this.useDomainName))
1816 return false;
1817
1818 if (!this.useDomainName)
1819 {
1820 this.Domain = string.Empty;
1821 this.DynamicDns = false;
1822 this.UseEncryption = false;
1823 this.CustomCA = false;
1824 this.ContactEMail = string.Empty;
1825 this.AcceptToS = false;
1826 }
1827 else
1828 {
1829 if (!this.TryGetEnvironmentVariable(GATEWAY_DOMAIN_NAME, true, out string Value))
1830 return true;
1831
1832 if (string.Compare(Value, "localhost", true) == 0)
1833 {
1834 this.LogEnvironmentError("localhost is not a valid domain name.", GATEWAY_DOMAIN_NAME, Value);
1835 return false;
1836 }
1837
1838 this.Domain = Value;
1839
1840 Value = Environment.GetEnvironmentVariable(GATEWAY_DOMAIN_ALT);
1841 if (string.IsNullOrEmpty(Value))
1842 this.AlternativeDomains = null;
1843 else
1844 {
1845 string[] Parts = Value.Split(',');
1846
1847 foreach (string Part in Parts)
1848 {
1849 if (string.Compare(Part, "localhost", true) == 0)
1850 {
1851 this.LogEnvironmentError("localhost is not a valid alternative domain name.", GATEWAY_DOMAIN_ALT, Value);
1852 return false;
1853 }
1854 }
1855
1856 this.AlternativeDomains = Parts;
1857 }
1858
1859 if (!this.TryGetEnvironmentVariable(GATEWAY_DYNDNS, out this.dynamicDns, false))
1860 return false;
1861
1862 if (this.dynamicDns)
1863 {
1864 if (!this.TryGetEnvironmentVariable(GATEWAY_DYNDNS_TEMPLATE, true, out Value))
1865 return false;
1866
1867 this.DynDnsTemplate = Value;
1868
1869 if (!this.TryGetEnvironmentVariable(GATEWAY_DYNDNS_CHECK, true, out Value))
1870 return false;
1871
1872 this.CheckIpScript = Value;
1873
1874 if (!this.TryGetEnvironmentVariable(GATEWAY_DYNDNS_UPDATE, true, out Value))
1875 return false;
1876
1877 this.UpdateIpScript = Value;
1878
1879 this.TryGetEnvironmentVariable(GATEWAY_DYNDNS_ACCOUNT, false, out this.dynDnsAccount);
1880 this.TryGetEnvironmentVariable(GATEWAY_DYNDNS_PASSWORD, false, out this.dynDnsPassword);
1881
1882 if (!this.TryGetEnvironmentVariable(GATEWAY_DYNDNS_UPDATE, 60, 86400, true, ref this.dynDnsInterval))
1883 return false;
1884 }
1885
1886 if (!this.TryGetEnvironmentVariable(GATEWAY_ENCRYPTION, true, out this.useEncryption))
1887 return false;
1888
1889 if (this.useEncryption)
1890 {
1891 if (!this.TryGetEnvironmentVariable(GATEWAY_CA_CUSTOM, out this.customCA, false))
1892 return false;
1893
1894 if (this.customCA)
1895 {
1896 if (!this.TryGetEnvironmentVariable(GATEWAY_ACME_DIRECTORY, true, out this.acmeDirectory))
1897 return false;
1898 }
1899
1900 if (!this.TryGetEnvironmentVariable(GATEWAY_ACME_EMAIL, true, out this.contactEMail))
1901 return false;
1902
1903 if (!this.TryGetEnvironmentVariable(GATEWAY_ACME_ACCEPT_TOS, true, out this.acceptToS))
1904 return false;
1905 }
1906 }
1907
1908 AlternativeField LocalizedString = this.GetLocalizedEnvironmentVariable(GATEWAY_HR_NAME, GATEWAY_HR_NAME_LANG);
1909 if (LocalizedString is null)
1910 return false;
1911
1912 this.HumanReadableName = LocalizedString.Value;
1913 this.HumanReadableNameLanguage = LocalizedString.Key;
1914
1915 LocalizedString = this.GetLocalizedEnvironmentVariable(GATEWAY_HR_DESC, GATEWAY_HR_DESC_LANG);
1916 if (LocalizedString is null)
1917 return false;
1918
1919 this.HumanReadableDescription = LocalizedString.Value;
1920 this.HumanReadableDescriptionLanguage = LocalizedString.Key;
1921
1922 this.localizedNames = this.GetLocalizedEnvironmentVariables(GATEWAY_HR_NAME_LOC, GATEWAY_HR_NAME_);
1923 if (this.localizedNames is null)
1924 return false;
1925
1926 this.localizedDescriptions = this.GetLocalizedEnvironmentVariables(GATEWAY_HR_DESC_LOC, GATEWAY_HR_DESC_);
1927 if (this.localizedDescriptions is null)
1928 return false;
1929
1930 if (!await this.Test(null, true)) // Tests domain names.
1931 return false;
1932
1933 if (this.dynamicDns && !await this.CheckDynamicIp(true)) // Tests dynamic DNS settings.
1934 return false;
1935
1936 if (this.useEncryption && this.useDomainName && !await this.CreateCertificate(null, true)) // Creates certificate.
1937 return false;
1938
1939 return true;
1940 }
1941
1942 private AlternativeField GetLocalizedEnvironmentVariable(string VariableName, string LanguageVariableName)
1943 {
1944 if (!this.TryGetEnvironmentVariable(VariableName, true, out string Value) ||
1945 !this.TryGetEnvironmentVariable(LanguageVariableName, true, out string Language))
1946 {
1947 return null;
1948 }
1949
1950 return new AlternativeField(Value, Language);
1951 }
1952
1953 private AlternativeField[] GetLocalizedEnvironmentVariables(string CollectionVariableName, string VariableName)
1954 {
1955 List<AlternativeField> Result = new List<AlternativeField>();
1956
1957 string Value = Environment.GetEnvironmentVariable(CollectionVariableName);
1958 if (!string.IsNullOrEmpty(Value))
1959 {
1960 string[] Languages = Value.Split(',');
1961 string Name;
1962
1963 foreach (string Language in Languages)
1964 {
1965 Name = VariableName + Language;
1966
1967 if (!this.TryGetEnvironmentVariable(Name, true, out Value))
1968 return null;
1969
1970 Result.Add(new AlternativeField(Language, Value));
1971 }
1972 }
1973
1974 return Result.ToArray();
1975 }
1976
1977 }
1978}
const string DefaultContentType
text/plain
Definition: BinaryCodec.cs:24
Static class managing loading of resources stored as embedded resources or in content files.
Definition: Resources.cs:15
static async Task< byte[]> ReadAllBytesAsync(string FileName)
Reads a binary file asynchronously.
Definition: Resources.cs:183
static Task WriteAllTextAsync(string FileName, string Text)
Creates a text file asynchronously.
Definition: Resources.cs:267
Plain text encoder/decoder.
const string DefaultContentType
text/plain
Helps with common XML-related tasks.
Definition: XML.cs:19
static string HtmlValueEncode(string s)
Differs from Encode(String), in that it does not encode the aposotrophe or the quote.
Definition: XML.cs:209
Static class managing the application event log. Applications and services log events on this static ...
Definition: Log.cs:13
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:1647
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:126
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:3041
static byte[] NextBytes(int NrBytes)
Generates an array of random bytes.
Definition: Gateway.cs:3534
static int[] GetConfigPorts(string Protocol)
Gets the port numbers defined for a given protocol in the configuration file.
Definition: Gateway.cs:2420
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 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
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.
override string Resource
Resource to be redirected to, to perform the configuration.
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.
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:18
HttpRequestHeader Header
Request header.
Definition: HttpRequest.cs:134
bool HasData
If the request has data.
Definition: HttpRequest.cs:74
string SubPath
Sub-path. If a resource is found handling the request, this property contains the trailing sub-path o...
Definition: HttpRequest.cs:146
async Task< object > DecodeDataAsync()
Decodes data sent in request.
Definition: HttpRequest.cs:95
Base class for all HTTP resources.
Definition: HttpResource.cs:23
Represets a response of an HTTP client request.
Definition: HttpResponse.cs:21
async Task Write(byte[] Data)
Returns binary data in the response.
Encoding Encoding
Gets the System.Text.Encoding in which the output is written.
Implements an HTTP server.
Definition: HttpServer.cs:36
static Variables CreateVariables()
Creates a new collection of variables, that contains access to the global set of variables.
Definition: HttpServer.cs:1604
HttpResource Register(HttpResource Resource)
Registers a resource with the server.
Definition: HttpServer.cs:1287
bool Unregister(HttpResource Resource)
Unregisters a resource from the server.
Definition: HttpServer.cs:1438
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:19
static async Task Update(object Object)
Updates an object in the database.
Definition: Database.cs:626
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:39
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
RSAParameters ExportAccountKey(bool IncludePrivateParameters)
Exports the account key.
Definition: AcmeClient.cs:754
async Task< AcmeAccount > GetAccount()
Gets the account object from the ACME server.
Definition: AcmeClient.cs:446
async Task< AcmeAccount > CreateAccount(string[] ContactURLs, bool TermsOfServiceAgreed)
Creates an account on the ACME server.
Definition: AcmeClient.cs:400
async Task< AcmeDirectory > GetDirectory()
Gets the ACME directory.
Definition: AcmeClient.cs:101
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:59
static string BinaryToString(byte[] Data)
Converts an array of bytes to a string with their hexadecimal representations (in lower case).
Definition: Hashes.cs:65
Contains information about a Certificate Signing Request (CSR).
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:72
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...
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