Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
FtpClientControlConnection.cs
1using System;
2using System.IO;
3using System.Net;
5using System.Runtime.ExceptionServices;
7using System.Security.Authentication;
8using System.Text;
9using System.Threading;
10using System.Threading.Tasks;
11using Waher.Content;
12using Waher.Events;
16using Waher.Security;
18
20{
25 {
26 private const int MaxLineLength = 1024;
27
28 private FtpControlConnectionState state;
29 private string user = string.Empty;
30 private readonly MemoryStream incoming = new MemoryStream();
31 private int incomingSize = 0;
32 private string rootFolder = string.Empty;
33 private string localFolder = string.Empty;
34 private string renameFrom = string.Empty;
35 private string selectedHostName = string.Empty;
36 private long maxStorage = 0;
37 private long currentStorage = 0;
38 private AsyncQueue<PassiveRecord> passiveStreams = null;
39 private RepresentationType type = RepresentationType.AsciiNonPrint;
40 private TransferMode mode = TransferMode.Stream;
41 private ProtectionLevel protectionLevel = ProtectionLevel.Clear;
42 private IPEndPoint ipExtension = null;
43 private CancellationTokenSource cancelToken = new CancellationTokenSource();
44 private TaskCompletionSource<bool> cancelled = new TaskCompletionSource<bool>();
45 private readonly IPEndPoint localEndpoint;
46 private readonly IPEndPoint remoteEndpoint;
47 private readonly ClientCertificates clientCertificates;
48 private readonly bool trustCertificates;
49 private long? restartPoint = null;
50 private bool utf8 = false;
51 private bool onlyPassive = false;
52
64 bool TrustCertificates, params ISniffer[] Sniffers)
65 : base(Client, Server, Persistence, Sniffers)
66 {
67 this.state = FtpControlConnectionState.Initiating;
68 this.localEndpoint = (IPEndPoint)Client.Client.Client.LocalEndPoint;
69 this.remoteEndpoint = (IPEndPoint)Client.Client.Client.RemoteEndPoint;
70 this.clientCertificates = ClientCertificates;
71 this.trustCertificates = TrustCertificates;
72 }
73
77 public FtpControlConnectionState State => this.state;
78
82 internal async Task SetState(FtpControlConnectionState NewState)
83 {
84 if (this.state != NewState && !this.Disposed)
85 {
86 this.state = NewState;
87
88 this.Information("State changed to " + NewState.ToString());
89
90 await this.OnStateChanged.Raise(this, NewState);
91 }
92 }
93
97 public event EventHandlerAsync<FtpControlConnectionState> OnStateChanged = null;
98
102 public async override Task DisposeAsync()
103 {
104 if (!this.Disposed)
105 {
106 if (this.state != FtpControlConnectionState.Error)
107 await this.SetState(FtpControlConnectionState.Offline);
108
109 this.cancelToken?.Cancel();
110 this.cancelToken?.Dispose();
111 this.cancelToken = null;
112
113 this.passiveStreams?.Dispose();
114 this.passiveStreams = null;
115
116 await base.DisposeAsync();
117 }
118 }
119
123 protected override async Task ErrorAndClose()
124 {
125 await this.SetState(FtpControlConnectionState.Error);
126 await base.ErrorAndClose();
127 }
128
137 protected async override Task<bool> ParseIncoming(bool ConstantBuffer, byte[] Data, int Offset, int NrRead)
138 {
139 int End = Offset + NrRead;
140 byte b;
141
142 while (Offset < End)
143 {
144 b = Data[Offset++];
145
146 if (b == 10) // LF
147 {
148 string Row = Encoding.UTF8.GetString(this.incoming.ToArray());
149 this.incoming.Position = 0;
150 this.incoming.SetLength(0);
151 this.incomingSize = 0;
152
153 if (Row.StartsWith("PASS ", StringComparison.CurrentCultureIgnoreCase))
154 this.ReceiveText("PASS <hidden>");
155 else
156 this.ReceiveText(Row);
157
158 if (!await this.ParseIncomingRow(Row))
159 return false;
160 }
161 else if (b != 13) // Not CR
162 {
163 this.incoming.WriteByte(b);
164 this.incomingSize++;
165
166 if (this.incomingSize > MaxLineLength)
167 {
168 await this.BeginWrite("552 line too long.\r\n", async (Sender, e) =>
169 {
170 await this.SetState(FtpControlConnectionState.Offline);
171 this.Server.Closed(this);
172 }, null);
173 return false;
174 }
175 }
176 }
177
178 return true;
179 }
180
181 private static void GetCommand(string s, out string Cmd, out string Value)
182 {
183 int i = s.IndexOf(' ');
184
185 if (i < 0)
186 {
187 Cmd = s;
188 Value = string.Empty;
189 }
190 else
191 {
192 Cmd = s[..i];
193 Value = s[(i + 1)..];
194 }
195 }
196
197 private async Task<bool> ParseIncomingRow(string s)
198 {
199 try
200 {
201 this.Server.Ping(this);
202
203 GetCommand(s, out string Cmd, out string Value);
204
205 int i;
206 bool b;
207 string s2;
208 StringBuilder sb;
209
210 switch (Cmd.ToUpper())
211 {
212 // Mandatory commands:
213
214 case "USER":
215 this.user = Value.Trim();
216
218
219 if (!(this.Client.RemoteCertificate is null) &&
220 (this.Client.RemoteCertificateValid || this.trustCertificates) &&
221 this.user == BinaryTcpClient.GetDomainFromSubject(this.Client.RemoteCertificate.Subject))
222 {
223 Account = await this.Server.PersistenceLayer.GetAccount(this.user);
224 if (Account is null)
225 {
226 LoginAuditor.Fail("Invalid user account provided.", this.user, this.RemoteEndPoint, "FTP");
227 return await this.SaslErrorNotAuthorized();
228 }
229 else if (!Account.Enabled)
230 {
231 LoginAuditor.Fail("Account disabled.", this.user, this.RemoteEndPoint, "FTP");
232 return await this.SaslErrorAccountDisabled();
233 }
234
235 string Msg = await this.CanLogin();
236 if (!string.IsNullOrEmpty(Msg))
237 return await this.BeginWrite(Msg, null, null);
238
239 this.rootFolder = await this.Server.PersistenceLayer.GetRootFolder(this.user);
240 if (string.IsNullOrEmpty(this.rootFolder))
241 {
242 LoginAuditor.Fail("Access to FTP not authorized.", this.user, this.RemoteEndPoint, "FTP");
243 return await this.SaslErrorNotAuthorized();
244 }
245
246 LoginAuditor.Success("User logged in.", this.user, this.RemoteEndPoint, "FTP");
247
248 if (this.rootFolder.EndsWith(Path.DirectorySeparatorChar))
249 this.rootFolder = this.rootFolder[..^1];
250 this.localFolder = new string(Path.DirectorySeparatorChar, 1);
251
252 await this.SetAccount(Account);
253 await this.SetState(FtpControlConnectionState.Authenticated);
254 await this.SaslSuccess(null);
255
256 return true;
257 }
258 else
259 {
260 if (!await this.BeginWrite("331 User name okay, need password.\r\n", null, null))
261 return false;
262
263 await this.SetState(FtpControlConnectionState.Authenticating);
264 return true;
265 }
266
267 case "PASS":
268 if (string.IsNullOrEmpty(this.user))
269 return await this.BeginWrite("503 User name not provided.\r\n", null, null);
270
271 Account = await this.Server.PersistenceLayer.GetAccount(this.user);
272 if (Account is null)
273 {
274 LoginAuditor.Fail("Invalid user account provided.", this.user, this.RemoteEndPoint, "FTP");
275 return await this.SaslErrorNotAuthorized();
276 }
277 else if (!Account.Enabled)
278 {
279 LoginAuditor.Fail("Account disabled.", this.user, this.RemoteEndPoint, "FTP");
280 return await this.SaslErrorAccountDisabled();
281 }
282 else
283 {
284 string Msg = await this.CanLogin();
285 if (!string.IsNullOrEmpty(Msg))
286 return await this.BeginWrite(Msg, null, null);
287
288 if (!this.IsEncrypted && this.Server.EncryptionRequired)
289 return await this.BeginWrite("534 Policy requires encryption.\r\n", null, null);
290
291 if (Account.Password != Value)
292 {
293 LoginAuditor.Fail("Invalid user credentials provided.", this.user, this.RemoteEndPoint, "FTP");
294 return await this.SaslErrorNotAuthorized();
295 }
296
297 this.rootFolder = await this.Server.PersistenceLayer.GetRootFolder(this.user);
298 if (string.IsNullOrEmpty(this.rootFolder))
299 {
300 LoginAuditor.Fail("Access to FTP not authorized.", this.user, this.RemoteEndPoint, "FTP");
301 return await this.SaslErrorNotAuthorized();
302 }
303
304 LoginAuditor.Success("User logged in.", this.user, this.RemoteEndPoint, "FTP");
305
306 if (this.rootFolder.EndsWith(Path.DirectorySeparatorChar))
307 this.rootFolder = this.rootFolder[..^1];
308 this.localFolder = new string(Path.DirectorySeparatorChar, 1);
309
310 await this.SetAccount(Account);
311 await this.SetState(FtpControlConnectionState.Authenticated);
312 await this.SaslSuccess(null);
313
314 return true;
315 }
316
317 case "TYPE":
318 GetCommand(Value, out string Option, out string OptionValue);
319 switch (Option.ToLower())
320 {
321 case "a":
322 if (string.IsNullOrEmpty(OptionValue))
323 this.type = RepresentationType.Ascii;
324 else
325 {
326 switch (OptionValue.ToLower())
327 {
328 case "n":
329 this.type = RepresentationType.AsciiNonPrint;
330 break;
331
332 case "t":
333 this.type = RepresentationType.AsciiTelnetFormatEffectors;
334 break;
335
336 case "c":
337 this.type = RepresentationType.AsciiCarriageControl;
338 break;
339
340 default:
341 return await this.BeginWrite("451 Invalid type.\r\n", null, null);
342 }
343 }
344 break;
345
346 case "e":
347 if (string.IsNullOrEmpty(OptionValue))
348 this.type = RepresentationType.Ebcdic;
349 else
350 {
351 switch (OptionValue.ToLower())
352 {
353 case "n":
354 this.type = RepresentationType.EbcdicNonPrint;
355 break;
356
357 case "t":
358 this.type = RepresentationType.EbcdicTelnetFormatEffectors;
359 break;
360
361 case "c":
362 this.type = RepresentationType.EbcdicCarriageControl;
363 break;
364
365 default:
366 return await this.BeginWrite("451 Invalid type.\r\n", null, null);
367 }
368 }
369 break;
370
371 case "i":
372 if (string.IsNullOrEmpty(OptionValue))
373 this.type = RepresentationType.Image;
374 else
375 return await this.BeginWrite("451 Invalid type.\r\n", null, null);
376 break;
377
378 case "l":
379 if (!int.TryParse(OptionValue, out i))
380 return await this.BeginWrite("451 Invalid type.\r\n", null, null);
381 else if (i != 8)
382 return await this.BeginWrite("504 Unhandled option.\r\n", null, null);
383 else
384 this.type = RepresentationType.LocalByteSize8;
385 break;
386
387 default:
388 return await this.BeginWrite("504 Unrecognized option.\r\n", null, null);
389 }
390 break;
391
392 case "MODE":
393 switch (Value.ToLower())
394 {
395 case "s":
396 this.mode = TransferMode.Stream;
397 break;
398
399 case "b":
400 this.mode = TransferMode.Block;
401 break;
402
403 case "c":
404 this.mode = TransferMode.Compressed;
405 break;
406
407 default:
408 return await this.BeginWrite("504 Unrecognized option.\r\n", null, null);
409 }
410 break;
411
412 case "QUIT":
413 if (!await this.BeginWrite("200 OK\r\n", null, null))
414 return false;
415
416 this.Server?.Closed(this);
417 await this.DisposeAsync();
418
419 return false;
420
421 case "PORT":
422 if (string.IsNullOrEmpty(Value))
423 return await this.BeginWrite("501 Missing parameter.\r\n", null, null);
424
425 string[] P = Value.Split(',');
426
427 if (P.Length != 6 ||
428 !byte.TryParse(P[0], out byte h1) ||
429 !byte.TryParse(P[1], out byte h2) ||
430 !byte.TryParse(P[2], out byte h3) ||
431 !byte.TryParse(P[3], out byte h4) ||
432 !byte.TryParse(P[4], out byte p1) ||
433 !byte.TryParse(P[5], out byte p2))
434 {
435 return await this.BeginWrite("501 Syntax error.\r\n", null, null);
436 }
437
438 if (this.onlyPassive)
439 return await this.BeginWrite("522 Passive mode only.\r\n", null, null);
440
441 this.ipExtension = new IPEndPoint(
442 new IPAddress(new byte[] { h1, h2, h3, h4 }),
443 p1 << 8 | p2);
444
445 break;
446
447 case "PASV":
448 DataPort DataPort = await this.BeginListen(null, 10000);
449
450 if (DataPort is null)
451 return await this.BeginWrite("522 Unable to open port listening for incoming connections.\r\n", null, null);
452
453 p1 = (byte)(DataPort.Port >> 8);
454 p2 = (byte)(DataPort.Port & 255);
455
456 sb = new StringBuilder();
457 sb.Append("227 Entering Passive Mode (");
458 sb.Append(this.localEndpoint.Address.ToString().Replace('.', ','));
459 sb.Append(',');
460 sb.Append(p1.ToString());
461 sb.Append(',');
462 sb.Append(p2.ToString());
463 sb.Append(")\r\n");
464
465 return await this.BeginWrite(sb.ToString(), null, null);
466
467 case "REST":
468 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
469 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
470
471 if (!long.TryParse(Value, out long l))
472 return await this.BeginWrite("501 Invalid file position.\r\n", null, null);
473 else if (l < 0)
474 return await this.BeginWrite("501 File positions must be positive.\r\n", null, null);
475
476 this.restartPoint = l;
477 return await this.BeginWrite("350 Restarting at " + l.ToString() + " for next REST, STOR or APPE.\r\n", null, null);
478
479 case "RETR":
480 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
481 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
482
483 if (string.IsNullOrEmpty(Value))
484 return await this.BeginWrite("501 File path missing.\r\n", null, null);
485 else if (HasForbiddenCharacters(Value))
486 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
487 else
488 s2 = Value;
489
490 s2 = this.GetFullPath(s2);
491 if (string.IsNullOrEmpty(s2))
492 return await this.BeginWrite("550 Access denied.\r\n", null, null);
493
494 if (!File.Exists(s2))
495 return await this.BeginWrite("550 File not found.\r\n", null, null);
496 else if (!this.Account.HasPrivilege(ReadPrivilege(s2)))
497 return await this.BeginWrite("550 Access denied.\r\n", null, null);
498
499 FileStream f;
500
501 try
502 {
503 f = File.OpenRead(s2);
504 }
505 catch (Exception ex)
506 {
507 return await this.BeginWrite("550 Access denied: " + FirstRow(ex) + "\r\n", null, null);
508 }
509
510 return await this.SendBinaryFile(f, null, true);
511
512 case "STOR":
513 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
514 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
515
516 if (string.IsNullOrEmpty(Value))
517 return await this.BeginWrite("501 File path missing.\r\n", null, null);
518 else if (HasForbiddenCharacters(Value))
519 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
520 else
521 s2 = Value;
522
523 s2 = this.GetFullPath(s2);
524 if (string.IsNullOrEmpty(s2))
525 return await this.BeginWrite("550 Access denied.\r\n", null, null);
526
527 if (!this.Account.HasPrivilege(WritePrivilege(s2)))
528 return await this.BeginWrite("550 Access denied.\r\n", null, null);
529
530 long InitialSize;
531
532 try
533 {
534 if (this.restartPoint.HasValue)
535 {
536 f = File.OpenWrite(s2); // restartPoint will be set in TransferBinaryFile
537 InitialSize = f.Length;
538 }
539 else
540 {
541 f = File.Create(s2);
542 InitialSize = 0;
543 }
544 }
545 catch (Exception ex)
546 {
547 return await this.BeginWrite("550 Access denied: " + FirstRow(ex) + "\r\n", null, null);
548 }
549
550 return await this.ReceiveBinaryFile(f, true, InitialSize);
551
552 case "APPE":
553 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
554 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
555
556 if (string.IsNullOrEmpty(Value))
557 return await this.BeginWrite("501 File path missing.\r\n", null, null);
558 else if (HasForbiddenCharacters(Value))
559 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
560 else
561 s2 = Value;
562
563 s2 = this.GetFullPath(s2);
564 if (string.IsNullOrEmpty(s2))
565 return await this.BeginWrite("550 Access denied.\r\n", null, null);
566
567 if (!this.Account.HasPrivilege(WritePrivilege(s2)))
568 return await this.BeginWrite("550 Access denied.\r\n", null, null);
569
570 try
571 {
572 if (File.Exists(s2))
573 {
574 f = File.OpenWrite(s2); // restartPoint will be set in TransferBinaryFile
575 InitialSize = f.Length;
576 }
577 else
578 {
579 f = File.Create(s2);
580 InitialSize = 0;
581 }
582 }
583 catch (Exception ex)
584 {
585 return await this.BeginWrite("550 Access denied: " + FirstRow(ex) + "\r\n", null, null);
586 }
587
588 return await this.ReceiveBinaryFile(f, true, InitialSize);
589
590 case "DELE":
591 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
592 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
593
594 if (string.IsNullOrEmpty(Value))
595 return await this.BeginWrite("501 File path missing.\r\n", null, null);
596 else if (HasForbiddenCharacters(Value))
597 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
598 else
599 s2 = Value;
600
601 s2 = this.GetFullPath(s2);
602 if (string.IsNullOrEmpty(s2))
603 return await this.BeginWrite("550 Access denied.\r\n", null, null);
604
605 if (!File.Exists(s2))
606 return await this.BeginWrite("550 File not found.\r\n", null, null);
607 else if (!this.Account.HasPrivilege(WritePrivilege(s2)))
608 return await this.BeginWrite("550 Access denied.\r\n", null, null);
609
610 try
611 {
612 FileInfo Info = new FileInfo(s2);
613 long Len = Info.Length;
614
615 this.restartPoint = null;
616 File.Delete(s2);
617
618 this.currentStorage -= Len;
619 }
620 catch (Exception ex)
621 {
622 return await this.BeginWrite("550 Access denied: " + FirstRow(ex) + "\r\n", null, null);
623 }
624
625 return await this.BeginWrite("250 File deleted.\r\n", null, null);
626
627 case "RNFR":
628 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
629 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
630
631 if (string.IsNullOrEmpty(Value))
632 return await this.BeginWrite("501 File path missing.\r\n", null, null);
633 else if (HasForbiddenCharacters(Value))
634 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
635 else
636 s2 = Value;
637
638 s2 = this.GetFullPath(s2);
639 if (string.IsNullOrEmpty(s2))
640 return await this.BeginWrite("550 Access denied.\r\n", null, null);
641
642 if (!File.Exists(s2) && !Directory.Exists(s2))
643 return await this.BeginWrite("550 File not found.\r\n", null, null);
644 else if (!this.Account.HasPrivilege(WritePrivilege(s2)))
645 return await this.BeginWrite("550 Access denied.\r\n", null, null);
646
647 this.renameFrom = s2;
648
649 return await this.BeginWrite("350 File or folder exists, ready for destination name.\r\n", null, null);
650
651 case "RNTO":
652 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
653 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
654
655 if (string.IsNullOrEmpty(Value))
656 return await this.BeginWrite("501 File path missing.\r\n", null, null);
657 else if (HasForbiddenCharacters(Value))
658 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
659 else
660 s2 = Value;
661
662 s2 = this.GetFullPath(s2);
663 if (string.IsNullOrEmpty(s2))
664 return await this.BeginWrite("550 Access denied.\r\n", null, null);
665
666 if (string.IsNullOrEmpty(this.renameFrom))
667 return await this.BeginWrite("503 Need RNFR first.\r\n", null, null);
668
669 if (!this.Account.HasPrivilege(WritePrivilege(this.renameFrom)))
670 return await this.BeginWrite("550 Access denied.\r\n", null, null);
671
672 try
673 {
674 if (File.Exists(this.renameFrom))
675 {
676 File.Move(this.renameFrom, s2);
677 this.renameFrom = null;
678 return await this.BeginWrite("250 File renamed.\r\n", null, null);
679 }
680 else if (Directory.Exists(this.renameFrom))
681 {
682 Directory.Move(this.renameFrom, s2);
683 this.renameFrom = null;
684 return await this.BeginWrite("250 Folder renamed.\r\n", null, null);
685 }
686 else
687 {
688 this.renameFrom = null;
689 return await this.BeginWrite("550 File or folder no longer found.\r\n", null, null);
690 }
691 }
692 catch (IOException ex)
693 {
694 return await this.BeginWrite("450 Unable to rename file: " + FirstRow(ex) + "\r\n", null, null);
695 }
696
697 case "ABOR":
698 this.cancelToken?.Cancel();
699
700 TaskCompletionSource<bool> Cancelled = this.cancelled;
701 Task _ = Task.Delay(1000).ContinueWith((_) => Cancelled.TrySetResult(false));
702
703 await Cancelled.Task;
704
705 return await this.BeginWrite("226 Abort successful.\r\n", null, null);
706
707 case "REIN":
708 this.ResetState(false);
709 break;
710
711 case "STRU":
712 if (string.IsNullOrEmpty(Value))
713 return await this.BeginWrite("200 File Structure (F) used.\r\n", null, null);
714
715 if (Value.ToLower() == "f")
716 break;
717 else
718 return await this.BeginWrite("504 Unsupported option.\r\n", null, null);
719
720 case "ACCT":
721 return await this.BeginWrite("202 Account not used.\r\n", null, null);
722
723 case "ALLO":
724 return await this.BeginWrite("202 No need to allocated space for files.\r\n", null, null);
725
726 case "SITE":
727 return await this.BeginWrite("202 No site-specific parameters.\r\n", null, null);
728
729 case "STAT":
730 return await this.BeginWrite("202 No status to report.\r\n", null, null);
731
732 case "NOOP":
733 break;
734
735 case "HELP":
736 return await this.BeginWrite("214 No help available.\r\n", null, null);
737
738 // Optional commands:
739
740 case "PWD":
741 if (string.IsNullOrEmpty(this.localFolder))
742 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
743 else
744 {
745 return await this.BeginWrite("257 \"" +
746 this.localFolder.Replace(Path.DirectorySeparatorChar, '/') +
747 "\" is current folder.\r\n", null, null);
748 }
749
750 case "CWD":
751 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
752 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
753
754 if (string.IsNullOrEmpty(Value))
755 s2 = this.localFolder;
756 else if (HasForbiddenCharacters(Value))
757 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
758 else
759 s2 = Value;
760
761 s2 = this.GetFullPath(s2);
762 if (string.IsNullOrEmpty(s2))
763 return await this.BeginWrite("550 Access denied.\r\n", null, null);
764
765 if (!Directory.Exists(s2))
766 return await this.BeginWrite("550 Folder not found.\r\n", null, null);
767 else if (!this.Account.HasPrivilege(ReadPrivilege(s2)))
768 return await this.BeginWrite("550 Access denied.\r\n", null, null);
769
770 this.localFolder = s2[this.rootFolder.Length..];
771 if (!this.localFolder.EndsWith(Path.DirectorySeparatorChar))
772 this.localFolder += Path.DirectorySeparatorChar;
773 break;
774
775 case "CDUP":
776 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
777 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
778
779 s2 = this.GetFullPath("..");
780 if (string.IsNullOrEmpty(s2))
781 return await this.BeginWrite("550 Access denied.\r\n", null, null);
782
783 if (s2.Length < this.rootFolder.Length)
784 return await this.BeginWrite("200 Already at root.\r\n", null, null);
785
786 if (!this.Account.HasPrivilege(ReadPrivilege(s2)))
787 return await this.BeginWrite("550 Access denied.\r\n", null, null);
788
789 this.localFolder = s2[this.rootFolder.Length..];
790 if (!this.localFolder.EndsWith(Path.DirectorySeparatorChar))
791 this.localFolder += Path.DirectorySeparatorChar;
792 break;
793
794 case "NLST":
795 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
796 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
797
798 if (string.IsNullOrEmpty(Value))
799 s2 = this.localFolder;
800 else if (HasForbiddenCharacters(Value))
801 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
802 else
803 s2 = Value;
804
805 s2 = this.GetFullPath(s2);
806 if (string.IsNullOrEmpty(s2))
807 return await this.BeginWrite("550 Access denied.\r\n", null, null);
808
809 sb = new StringBuilder();
810
811 if (Directory.Exists(s2))
812 {
813 if (!this.Account.HasPrivilege(ReadPrivilege(s2)))
814 return await this.BeginWrite("550 Access denied.\r\n", null, null);
815
816 foreach (string SubFolder in Directory.GetDirectories(s2, "*.*", SearchOption.TopDirectoryOnly))
817 {
818 if (!this.Account.HasPrivilege(ReadPrivilege(SubFolder)))
819 continue;
820
821 string Name = SubFolder[s2.Length..];
822
823 sb.Append(Name);
824 sb.Append("\r\n");
825 }
826
827 foreach (string FileName in Directory.GetFiles(s2, "*.*", SearchOption.TopDirectoryOnly))
828 {
829 if (!this.Account.HasPrivilege(ReadPrivilege(FileName)))
830 continue;
831
832 string Name = FileName[s2.Length..];
833
834 sb.Append(Name);
835 sb.Append("\r\n");
836 }
837 }
838 else if (File.Exists(s2))
839 {
840 if (!this.Account.HasPrivilege(ReadPrivilege(s2)))
841 return await this.BeginWrite("550 Access denied.\r\n", null, null);
842
843 sb.Append(Path.GetFileName(s2));
844 sb.Append("\r\n");
845 }
846 else
847 return await this.BeginWrite("501 File or folder not found.\r\n", null, null);
848
849 return await this.SendTextFile(sb.ToString());
850
851 case "LIST":
852 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
853 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
854
855 if (string.IsNullOrEmpty(Value))
856 s2 = this.localFolder;
857 else if (HasForbiddenCharacters(Value))
858 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
859 else
860 s2 = Value;
861
862 s2 = this.GetFullPath(s2);
863 if (string.IsNullOrEmpty(s2))
864 return await this.BeginWrite("550 Access denied.\r\n", null, null);
865
866 sb = new StringBuilder();
867
868 if (Directory.Exists(s2))
869 {
870 if (!this.Account.HasPrivilege(ReadPrivilege(s2)))
871 return await this.BeginWrite("550 Access denied.\r\n", null, null);
872
873 foreach (string SubFolder in Directory.GetDirectories(s2, "*.*", SearchOption.TopDirectoryOnly))
874 {
875 if (!this.Account.HasPrivilege(ReadPrivilege(SubFolder)))
876 continue;
877
878 AppendListInfo(sb, new DirectoryInfo(SubFolder));
879 }
880
881 foreach (string FileName in Directory.GetFiles(s2, "*.*", SearchOption.TopDirectoryOnly))
882 {
883 if (!this.Account.HasPrivilege(ReadPrivilege(FileName)))
884 continue;
885
886 AppendListInfo(sb, new FileInfo(FileName));
887 }
888 }
889 else if (File.Exists(s2))
890 {
891 if (!this.Account.HasPrivilege(ReadPrivilege(s2)))
892 return await this.BeginWrite("550 Access denied.\r\n", null, null);
893
894 AppendListInfo(sb, new FileInfo(s2));
895 }
896 else
897 return await this.BeginWrite("501 File or folder not found.\r\n", null, null);
898
899 return await this.SendTextFile(sb.ToString());
900
901 case "SYST":
902 if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
903 s2 = "WINDOWS";
904 else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
905 s2 = "LINUX";
906 else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
907 s2 = "MACOS"; // macOS is based on Darwin
908 else
909 s2 = "UNKNOWN";
910
911 return await this.BeginWrite("215 " + s2 + " Type: L8\r\n", null, null);
912
913 case "SMNT":
914 return await this.BeginWrite("502 Mounting file data structures not supported.\r\n", null, null);
915
916 case "MKD":
917 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
918 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
919
920 if (string.IsNullOrEmpty(Value))
921 s2 = this.localFolder;
922 else if (HasForbiddenCharacters(Value))
923 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
924 else
925 s2 = Value;
926
927 s2 = this.GetFullPath(s2);
928 if (string.IsNullOrEmpty(s2))
929 return await this.BeginWrite("550 Access denied.\r\n", null, null);
930
931 if (Directory.Exists(s2))
932 return await this.BeginWrite("550 Folder already exists.\r\n", null, null);
933 else if (!this.Account.HasPrivilege(WritePrivilege(s2)))
934 return await this.BeginWrite("550 Access denied.\r\n", null, null);
935 else
936 {
937 Directory.CreateDirectory(s2);
938 return await this.BeginWrite("257 Folder created.\r\n", null, null);
939 }
940
941 case "RMD":
942 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
943 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
944
945 if (string.IsNullOrEmpty(Value))
946 s2 = this.localFolder;
947 else if (HasForbiddenCharacters(Value))
948 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
949 else
950 s2 = Value;
951
952 s2 = this.GetFullPath(s2);
953 if (string.IsNullOrEmpty(s2))
954 return await this.BeginWrite("550 Access denied.\r\n", null, null);
955
956 if (!Directory.Exists(s2))
957 return await this.BeginWrite("550 No such folder.\r\n", null, null);
958 else if (!this.Account.HasPrivilege(WritePrivilege(s2)))
959 return await this.BeginWrite("550 Access denied.\r\n", null, null);
960 else
961 {
962 Directory.Delete(s2, false); // No recursive deletion in FTP
963 return await this.BeginWrite("250 Folder deleted.\r\n", null, null);
964 }
965
966 case "STOU":
967 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
968 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
969
970 if (string.IsNullOrEmpty(Value))
971 s2 = "NewFile.bin";
972 else if (HasForbiddenCharacters(Value))
973 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
974 else
975 s2 = Value;
976
977 s2 = this.GetFullPath(s2);
978 if (string.IsNullOrEmpty(s2))
979 return await this.BeginWrite("550 Access denied.\r\n", null, null);
980
981 i = 1;
982
983 while ((File.Exists(s2) || Directory.Exists(s2)) && i < 65536)
984 {
985 i++;
986 int j = s2.LastIndexOf('.');
987 if (j < 0)
988 j = s2.Length;
989
990 s2 = s2[0..j] + i.ToString() + s2[j..];
991 }
992
993 if (i >= 65536)
994 return await this.BeginWrite("550 Unable to find unique name.\r\n", null, null);
995
996 try
997 {
998 if (this.restartPoint.HasValue)
999 f = File.OpenWrite(s2); // restartPoint will be set in TransferBinaryFile
1000 else
1001 f = File.Create(s2);
1002 }
1003 catch (Exception ex)
1004 {
1005 return await this.BeginWrite("550 Access denied: " + FirstRow(ex) + "\r\n", null, null);
1006 }
1007
1008 return await this.ReceiveBinaryFile(f, true, 0);
1009
1010 // Optional commands:
1011
1012 case "ADAT": // TODO: RFC 2228
1013 case "CCC": // TODO: RFC 2228
1014 case "MIC": // TODO: RFC 2228
1015 case "CONF": // TODO: RFC 2228
1016 case "ENC": // TODO: RFC 2228
1017
1018 default:
1019 return await this.BeginWrite("502 Command not implemented.\r\n", null, null);
1020
1021 // Extensions:
1022
1023 case "OPTS": // RFC 2389
1024 GetCommand(Value, out Option, out OptionValue);
1025
1026 switch (Option.ToLower())
1027 {
1028 case "utf8":
1029 if (CommonTypes.TryParse(OptionValue, out b))
1030 this.utf8 = b;
1031 else
1032 return await this.BeginWrite("451 Invalid Boolean value.\r\n", null, null);
1033 break;
1034
1035 default:
1036 return await this.BeginWrite("504 Unrecognized option.\r\n", null, null);
1037 }
1038 break;
1039
1040 case "EPRT": // RFC 2428
1041 if (string.IsNullOrEmpty(Value))
1042 return await this.BeginWrite("501 Missing parameter.\r\n", null, null);
1043
1044 char Delimiter = Value[0];
1045 P = Value.Split(Delimiter);
1046
1047 if (P.Length != 5 || !string.IsNullOrEmpty(P[0]) || !string.IsNullOrEmpty(P[4]))
1048 return await this.BeginWrite("501 Syntax error.\r\n", null, null);
1049
1050 if (!int.TryParse(P[1], out int AddressType) ||
1051 AddressType < 1 || AddressType > 2)
1052 {
1053 return await this.BeginWrite("504 Unsupported address family.\r\n", null, null);
1054 }
1055
1056 if (!IPAddress.TryParse(P[2], out IPAddress Address))
1057 return await this.BeginWrite("501 Invalid IP Address.\r\n", null, null);
1058
1059 AddressFamily Expected = AddressType == 1 ? AddressFamily.InterNetwork : AddressFamily.InterNetworkV6;
1060 if (Address.AddressFamily != Expected)
1061 return await this.BeginWrite("501 IP Address does not match address family.\r\n", null, null);
1062
1063 if (!int.TryParse(P[3], out int Port) || Port < 1 || Port > 65535)
1064 return await this.BeginWrite("501 Invalid port number.\r\n", null, null);
1065
1066 if (this.onlyPassive)
1067 return await this.BeginWrite("522 Passive mode only.\r\n", null, null);
1068
1069 this.ipExtension = new IPEndPoint(Address, Port);
1070 break;
1071
1072 case "EPSV": // RFC 2428
1073 int? RequestedPort;
1074
1075 if (string.IsNullOrEmpty(Value))
1076 RequestedPort = null;
1077 else if (int.TryParse(Value, out i))
1078 {
1079 if (i < 1 || i > 65535)
1080 return await this.BeginWrite("501 Invalid port number.\r\n", null, null);
1081
1082 RequestedPort = i;
1083 }
1084 else if (string.Compare(Value, "ALL", true) == 0)
1085 {
1086 RequestedPort = null;
1087 this.onlyPassive = true;
1088 }
1089 else
1090 return await this.BeginWrite("501 Unrecognized parameter.\r\n", null, null);
1091
1092 DataPort = await this.BeginListen(RequestedPort, 10000);
1093
1094 if (DataPort is null)
1095 return await this.BeginWrite("522 Unable to open port listening for incoming connections.\r\n", null, null);
1096 else
1097 return await this.BeginWrite("229 Listening for incoming connections (|||" + DataPort.Port.ToString() + "|).\r\n", null, null);
1098
1099 case "AUTH": // RFC 2228
1100 switch (Value.ToLower())
1101 {
1102 case "tls":
1103 if (this.Client.IsEncrypted)
1104 return await this.BeginWrite("503 Already encrypted.\r\n", null, null);
1105
1106 this.ResetState(false);
1107 if (await this.BeginWrite("234 Proceed.\r\n", null, null))
1108 this.UpgradeToTls = true;
1109 return false;
1110
1111 default:
1112 return await this.BeginWrite("504 Unrecognized mechanism.\r\n", null, null);
1113 }
1114
1115 case "PBSZ": // RFC 2228
1116 if (!int.TryParse(Value, out i) || i < 0)
1117 return await this.BeginWrite("501 protection buffer size.\r\n", null, null);
1118
1119 if (!this.IsEncrypted)
1120 return await this.BeginWrite("503 connection not encrypted.\r\n", null, null);
1121
1122 // TODO: Protection buffers.
1123 return await this.BeginWrite("200 PBSZ=0.\r\n", null, null);
1124
1125 case "PROT": // RFC 2228
1126
1127 switch (Value.ToLower())
1128 {
1129 case "c":
1130 if (!this.Server.AllowClearDataChannel)
1131 return await this.BeginWrite("534 Protection level not allowed.\r\n", null, null);
1132
1133 this.protectionLevel = ProtectionLevel.Clear;
1134 break;
1135
1136 case "s":
1137 if (!this.Server.AllowSafeDataChannel)
1138 return await this.BeginWrite("534 Protection level not allowed.\r\n", null, null);
1139
1140 this.protectionLevel = ProtectionLevel.Safe;
1141 break;
1142
1143 case "e":
1145 return await this.BeginWrite("534 Protection level not allowed.\r\n", null, null);
1146
1147 this.protectionLevel = ProtectionLevel.Confidential;
1148 break;
1149
1150 case "p":
1152 return await this.BeginWrite("534 Protection level not allowed.\r\n", null, null);
1153
1154 this.protectionLevel = ProtectionLevel.Private;
1155 break;
1156
1157 default:
1158 return await this.BeginWrite("504 Unrecognized mechanism.\r\n", null, null);
1159 }
1160 break;
1161
1162 case "MLST": // RFC 3659
1163 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
1164 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
1165
1166 if (string.IsNullOrEmpty(Value))
1167 s2 = this.localFolder;
1168 else if (HasForbiddenCharacters(Value))
1169 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
1170 else
1171 s2 = Value;
1172
1173 s2 = this.GetFullPath(s2);
1174 if (string.IsNullOrEmpty(s2))
1175 return await this.BeginWrite("550 Access denied.\r\n", null, null);
1176
1177 sb = new StringBuilder();
1178
1179 if (File.Exists(s2))
1180 {
1181 if (!this.Account.HasPrivilege(ReadPrivilege(s2)))
1182 return await this.BeginWrite("550 Access denied.\r\n", null, null);
1183
1184 sb.Append("250-File information:\r\n ");
1185 AppendListEntryInfo(sb, new FileInfo(s2));
1186 }
1187 else if (Directory.Exists(s2))
1188 {
1189 if (!this.Account.HasPrivilege(ReadPrivilege(s2)))
1190 return await this.BeginWrite("550 Access denied.\r\n", null, null);
1191
1192 sb.Append("250-Directory information:\r\n ");
1193 AppendListEntryInfo(sb, new DirectoryInfo(s2));
1194 }
1195 else
1196 return await this.BeginWrite("501 File or folder not found.\r\n", null, null);
1197
1198 sb.Append("250 OK\r\n");
1199
1200 return await this.BeginWrite(sb.ToString(), null, null);
1201
1202 case "MLSD": // RFC 3659
1203 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
1204 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
1205
1206 if (string.IsNullOrEmpty(Value))
1207 s2 = this.localFolder;
1208 else if (HasForbiddenCharacters(Value))
1209 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
1210 else
1211 s2 = Value;
1212
1213 s2 = this.GetFullPath(s2);
1214 if (string.IsNullOrEmpty(s2))
1215 return await this.BeginWrite("550 Access denied.\r\n", null, null);
1216
1217 sb = new StringBuilder();
1218
1219 if (Directory.Exists(s2))
1220 {
1221 if (!this.Account.HasPrivilege(ReadPrivilege(s2)))
1222 return await this.BeginWrite("550 Access denied.\r\n", null, null);
1223
1224 foreach (string SubFolder in Directory.GetDirectories(s2, "*.*", SearchOption.TopDirectoryOnly))
1225 {
1226 if (!this.Account.HasPrivilege(ReadPrivilege(SubFolder)))
1227 continue;
1228
1229 AppendListEntryInfo(sb, new DirectoryInfo(SubFolder));
1230 }
1231
1232 foreach (string FileName in Directory.GetFiles(s2, "*.*", SearchOption.TopDirectoryOnly))
1233 {
1234 if (!this.Account.HasPrivilege(ReadPrivilege(FileName)))
1235 continue;
1236
1237 AppendListEntryInfo(sb, new FileInfo(FileName));
1238 }
1239
1240 return await this.SendTextFile(sb.ToString());
1241 }
1242 else
1243 return await this.BeginWrite("501 Folder not found.\r\n", null, null);
1244
1245 case "MDTM": // RFC 3659
1246 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
1247 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
1248
1249 if (string.IsNullOrEmpty(Value))
1250 s2 = this.localFolder;
1251 else if (HasForbiddenCharacters(Value))
1252 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
1253 else
1254 s2 = Value;
1255
1256 s2 = this.GetFullPath(s2);
1257 if (string.IsNullOrEmpty(s2))
1258 return await this.BeginWrite("550 Access denied.\r\n", null, null);
1259
1260 sb = new StringBuilder();
1261
1262 if (File.Exists(s2))
1263 {
1264 if (!this.Account.HasPrivilege(ReadPrivilege(s2)))
1265 return await this.BeginWrite("550 Access denied.\r\n", null, null);
1266
1267 FileInfo Info = new FileInfo(s2);
1268
1269 return await this.BeginWrite("213 " +
1270 Info.LastWriteTimeUtc.ToString(dateTimeFormatRfc3659) +
1271 "\r\n", null, null);
1272 }
1273 else
1274 return await this.BeginWrite("501 File not found.\r\n", null, null);
1275
1276 case "SIZE": // RFC 3659
1277 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
1278 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
1279
1280 if (string.IsNullOrEmpty(Value))
1281 s2 = this.localFolder;
1282 else if (HasForbiddenCharacters(Value))
1283 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
1284 else
1285 s2 = Value;
1286
1287 s2 = this.GetFullPath(s2);
1288 if (string.IsNullOrEmpty(s2))
1289 return await this.BeginWrite("550 Access denied.\r\n", null, null);
1290
1291 sb = new StringBuilder();
1292
1293 if (File.Exists(s2))
1294 {
1295 if (!this.Account.HasPrivilege(ReadPrivilege(s2)))
1296 return await this.BeginWrite("550 Access denied.\r\n", null, null);
1297
1298 FileInfo Info = new FileInfo(s2);
1299
1300 return await this.BeginWrite("213 " + Info.Length.ToString() +
1301 "\r\n", null, null);
1302 }
1303 else
1304 return await this.BeginWrite("501 File not found.\r\n", null, null);
1305
1306 case "TVFS": // RFC 3659
1307 return await this.BeginWrite("502 Virtual File Stores not supported.\r\n", null, null);
1308
1309 case "FEAT": // RFC 2389
1310 sb = new StringBuilder();
1311
1312 sb.Append("211-Features:\r\n");
1313
1314 if (!this.Client.IsEncrypted)
1315 sb.Append(" AUTH TLS\r\n");
1316
1317 //if (this.state < FtpControlConnectionState.Authenticated)
1318 //{
1319 // foreach (IAuthenticationMechanism Mechanism in SaslModule.Mechanisms)
1320 // {
1321 // sb.Append(" AUTH");
1322 // sb.Append(Mechanism.Name);
1323 // sb.Append("\r\n");
1324 // }
1325 //}
1326
1327 sb.Append(" TYPE I;L8\r\n");
1328 sb.Append(" MDTM\r\n");
1329 sb.Append(" MLST Type*;Size*;Modify*;Create*;Media-Type*;\r\n");
1330 sb.Append(" SIZE\r\n");
1331 sb.Append(" REST STREAM\r\n");
1332 sb.Append(" XSHA256\r\n");
1333 sb.Append(" XSHA512\r\n");
1334 sb.Append(" HOST\r\n");
1335 sb.Append("211 End\r\n");
1336
1337 return await this.BeginWrite(sb.ToString(), null, null);
1338
1339 case "HOST": // RFC 7151
1340 this.selectedHostName = Value;
1341 return await this.BeginWrite("220 Host received.\r\n", null, null);
1342
1343 // Non-standardized (but common) commands:
1344
1345 case "XSHA1":
1346 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
1347 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
1348
1349 if (string.IsNullOrEmpty(Value))
1350 return await this.BeginWrite("501 File path missing.\r\n", null, null);
1351 else if (HasForbiddenCharacters(Value))
1352 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
1353 else
1354 s2 = Value;
1355
1356 s2 = this.GetFullPath(s2);
1357 if (string.IsNullOrEmpty(s2))
1358 return await this.BeginWrite("550 Access denied.\r\n", null, null);
1359
1360 if (!File.Exists(s2))
1361 return await this.BeginWrite("501 File or folder not found.\r\n", null, null);
1362 else if (!this.Account.HasPrivilege(ReadPrivilege(s2)))
1363 return await this.BeginWrite("550 Access denied.\r\n", null, null);
1364
1365 using (FileStream fs = File.OpenRead(s2))
1366 {
1367 return await this.BeginWrite("213 " + Hashes.ComputeSHA1HashString(fs) + "\r\n", null, null);
1368 }
1369
1370 case "XSHA256":
1371 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
1372 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
1373
1374 if (string.IsNullOrEmpty(Value))
1375 return await this.BeginWrite("501 File path missing.\r\n", null, null);
1376 else if (HasForbiddenCharacters(Value))
1377 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
1378 else
1379 s2 = Value;
1380
1381 s2 = this.GetFullPath(s2);
1382 if (string.IsNullOrEmpty(s2))
1383 return await this.BeginWrite("550 Access denied.\r\n", null, null);
1384
1385 if (!File.Exists(s2))
1386 return await this.BeginWrite("501 File or folder not found.\r\n", null, null);
1387 else if (!this.Account.HasPrivilege(ReadPrivilege(s2)))
1388 return await this.BeginWrite("550 Access denied.\r\n", null, null);
1389
1390 using (FileStream fs = File.OpenRead(s2))
1391 {
1392 return await this.BeginWrite("213 " + Hashes.ComputeSHA256HashString(fs) + "\r\n", null, null);
1393 }
1394
1395 case "XSHA512":
1396 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
1397 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
1398
1399 if (string.IsNullOrEmpty(Value))
1400 return await this.BeginWrite("501 File path missing.\r\n", null, null);
1401 else if (HasForbiddenCharacters(Value))
1402 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
1403 else
1404 s2 = Value;
1405
1406 s2 = this.GetFullPath(s2);
1407 if (string.IsNullOrEmpty(s2))
1408 return await this.BeginWrite("550 Access denied.\r\n", null, null);
1409
1410 if (!File.Exists(s2))
1411 return await this.BeginWrite("501 File or folder not found.\r\n", null, null);
1412 else if (!this.Account.HasPrivilege(ReadPrivilege(s2)))
1413 return await this.BeginWrite("550 Access denied.\r\n", null, null);
1414
1415 using (FileStream fs = File.OpenRead(s2))
1416 {
1417 return await this.BeginWrite("213 " + Hashes.ComputeSHA512HashString(fs) + "\r\n", null, null);
1418 }
1419
1420 case "XMD5":
1421 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
1422 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
1423
1424 if (string.IsNullOrEmpty(Value))
1425 return await this.BeginWrite("501 File path missing.\r\n", null, null);
1426 else if (HasForbiddenCharacters(Value))
1427 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
1428 else
1429 s2 = Value;
1430
1431 s2 = this.GetFullPath(s2);
1432 if (string.IsNullOrEmpty(s2))
1433 return await this.BeginWrite("550 Access denied.\r\n", null, null);
1434
1435 if (!File.Exists(s2))
1436 return await this.BeginWrite("501 File or folder not found.\r\n", null, null);
1437 else if (!this.Account.HasPrivilege(ReadPrivilege(s2)))
1438 return await this.BeginWrite("550 Access denied.\r\n", null, null);
1439
1440 using (FileStream fs = File.OpenRead(s2))
1441 {
1442 return await this.BeginWrite("213 " + Hashes.ComputeMD5HashString(fs) + "\r\n", null, null);
1443 }
1444
1445 case "XCRC":
1446 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
1447 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
1448
1449 if (string.IsNullOrEmpty(Value))
1450 return await this.BeginWrite("501 File path missing.\r\n", null, null);
1451 else if (HasForbiddenCharacters(Value))
1452 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
1453 else
1454 s2 = Value;
1455
1456 s2 = this.GetFullPath(s2);
1457 if (string.IsNullOrEmpty(s2))
1458 return await this.BeginWrite("550 Access denied.\r\n", null, null);
1459
1460 if (!File.Exists(s2))
1461 return await this.BeginWrite("501 File or folder not found.\r\n", null, null);
1462 else if (!this.Account.HasPrivilege(ReadPrivilege(s2)))
1463 return await this.BeginWrite("550 Access denied.\r\n", null, null);
1464
1465 using (FileStream fs = File.OpenRead(s2))
1466 {
1467 return await this.BeginWrite("213 " + ComputeCrc32(fs).ToString("x8") + "\r\n", null, null);
1468 }
1469
1470 case "HASH":
1471 if (string.IsNullOrEmpty(this.localFolder) || this.Account is null)
1472 return await this.BeginWrite("530 User not logged in.\r\n", null, null);
1473
1474 GetCommand(Value, out Option, out OptionValue);
1475
1476 if (string.IsNullOrEmpty(OptionValue))
1477 {
1478 OptionValue = Option;
1479 Option = "SHA-256";
1480 }
1481
1482 if (HasForbiddenCharacters(OptionValue))
1483 return await this.BeginWrite("550 Forbidden characters.\r\n", null, null);
1484 else
1485 s2 = OptionValue;
1486
1487 s2 = this.GetFullPath(s2);
1488 if (string.IsNullOrEmpty(s2))
1489 return await this.BeginWrite("550 Access denied.\r\n", null, null);
1490
1491 if (!File.Exists(s2))
1492 return await this.BeginWrite("501 File or folder not found.\r\n", null, null);
1493 else if (!this.Account.HasPrivilege(ReadPrivilege(s2)))
1494 return await this.BeginWrite("550 Access denied.\r\n", null, null);
1495
1496 using (FileStream fs = File.OpenRead(s2))
1497 {
1498 return Option.ToLower() switch
1499 {
1500 "sha-1" => await this.BeginWrite("213 " + Hashes.ComputeSHA1HashString(fs) + "\r\n", null, null),
1501 "sha-256" => await this.BeginWrite("213 " + Hashes.ComputeSHA256HashString(fs) + "\r\n", null, null),
1502 "sha-512" => await this.BeginWrite("213 " + Hashes.ComputeSHA512HashString(fs) + "\r\n", null, null),
1503 "md5" => await this.BeginWrite("213 " + Hashes.ComputeMD5HashString(fs) + "\r\n", null, null),
1504 _ => await this.BeginWrite("504 Unrecognized hash algorithm.\r\n", null, null),
1505 };
1506 }
1507 }
1508
1509 return await this.BeginWrite("200 OK\r\n", null, null);
1510 }
1511 catch (Exception ex)
1512 {
1513 return await this.BeginWrite("550 " + FirstRow(ex) + "\r\n", null, null);
1514 }
1515 }
1516
1517 private string GetFullPath(string LocalPath)
1518 {
1519 string s;
1520
1521 LocalPath = LocalPath.Replace('/', Path.DirectorySeparatorChar);
1522
1523 if (LocalPath.StartsWith(Path.DirectorySeparatorChar))
1524 s = this.rootFolder + LocalPath;
1525 else
1526 s = this.rootFolder + this.localFolder + LocalPath;
1527
1528 if (s.EndsWith(Path.DirectorySeparatorChar))
1529 s = s[..^1];
1530
1531 s = Path.GetFullPath(s);
1532
1533 if (s == this.rootFolder || s.StartsWith(this.rootFolder + Path.DirectorySeparatorChar))
1534 return s;
1535 else
1536 return null;
1537 }
1538
1539 private static bool HasForbiddenCharacters(string Path)
1540 {
1541 char LastChar = (char)0;
1542
1543 foreach (char ch in Path)
1544 {
1545 switch (ch)
1546 {
1547 case '/':
1548 case '\\':
1549 if (LastChar == ch)
1550 return true;
1551 break;
1552
1553 case ':':
1554 case '?':
1555 case '*':
1556 return true;
1557 }
1558
1559 LastChar = ch;
1560 }
1561
1562 return false;
1563 }
1564
1565 private static void AppendListInfo(StringBuilder sb, DirectoryInfo Info)
1566 {
1567 sb.Append('d');
1568 sb.Append("rwxr-xr-x 1 owner group ");
1569 AppendListInfo(sb, 4096, Info);
1570 }
1571
1572 private static void AppendListInfo(StringBuilder sb, FileInfo Info)
1573 {
1574 sb.Append('-');
1575 sb.Append("rw-r--r-- 1 owner group ");
1576 AppendListInfo(sb, Info.Length, Info);
1577 }
1578
1579 private static void AppendListInfo(StringBuilder sb, long Size, FileSystemInfo Info)
1580 {
1581 Append(sb, Size, 13);
1582 sb.Append(' ');
1583 sb.Append(Month(Info.LastWriteTime.Month));
1584 Append(sb, Info.LastWriteTime.Day, 3);
1585
1586 if (DateTime.Now.AddMonths(-6) < Info.LastWriteTime)
1587 {
1588 sb.Append(' ');
1589 sb.Append(Info.LastWriteTime.Hour.ToString("D2"));
1590 sb.Append(':');
1591 sb.Append(Info.LastWriteTime.Minute.ToString("D2"));
1592 }
1593 else
1594 Append(sb, Info.LastWriteTime.Year, 6);
1595
1596 sb.Append(' ');
1597 sb.Append(Info.Name);
1598 sb.Append("\r\n");
1599 }
1600
1601 private static void Append(StringBuilder sb, long Nr, int Len)
1602 {
1603 string s = Nr.ToString();
1604 if (s.Length < Len)
1605 sb.Append(new string(' ', Len - s.Length));
1606
1607 sb.Append(s);
1608 }
1609
1610 private static string Month(int Month)
1611 {
1612 return Month switch
1613 {
1614 1 => "Jan",
1615 2 => "Feb",
1616 3 => "Mar",
1617 4 => "Apr",
1618 5 => "May",
1619 6 => "Jun",
1620 7 => "Jul",
1621 8 => "Aug",
1622 9 => "Sep",
1623 10 => "Oct",
1624 11 => "Nov",
1625 12 => "Dec",
1626 _ => string.Empty,
1627 };
1628 }
1629
1630 private const string dateTimeFormatRfc3659 = "yyyyMMddHHmmss";
1631
1632 private static string AppendListEntryInfo(StringBuilder sb, FileInfo Info)
1633 {
1634 sb.Append("Type=file;Size=");
1635 sb.Append(Info.Length.ToString());
1636 sb.Append(";Modify=");
1637 sb.Append(Info.LastWriteTimeUtc.ToString(dateTimeFormatRfc3659));
1638 sb.Append(";Create=");
1639 sb.Append(Info.CreationTimeUtc.ToString(dateTimeFormatRfc3659));
1640 sb.Append(";Media-Type=");
1641 sb.Append(InternetContent.GetContentType(Path.GetExtension(Info.Name)));
1642 sb.Append("; ");
1643 sb.Append(Info.Name);
1644 sb.Append("\r\n");
1645
1646 return sb.ToString();
1647 }
1648
1649 private static string AppendListEntryInfo(StringBuilder sb, DirectoryInfo Info)
1650 {
1651 sb.Append("Type=dir;Size=4096;Modify=");
1652 sb.Append(Info.LastWriteTimeUtc.ToString(dateTimeFormatRfc3659));
1653 sb.Append(";Create=");
1654 sb.Append(Info.CreationTimeUtc.ToString(dateTimeFormatRfc3659));
1655 sb.Append("; ");
1656 sb.Append(Info.Name);
1657 sb.Append("\r\n");
1658
1659 return sb.ToString();
1660 }
1661
1662 private static uint ComputeCrc32(Stream stream)
1663 {
1664 const uint Polynomial = 0xedb88320;
1665 uint[] Table = new uint[256];
1666 uint Crc;
1667
1668 for (uint i = 0; i < Table.Length; ++i)
1669 {
1670 Crc = i;
1671 for (uint j = 8; j > 0; --j)
1672 {
1673 if ((Crc & 1) == 1)
1674 Crc = (Crc >> 1) ^ Polynomial;
1675 else
1676 Crc >>= 1;
1677 }
1678
1679 Table[i] = Crc;
1680 }
1681
1682 Crc = 0xffffffff;
1683
1684 int b;
1685 while ((b = stream.ReadByte()) != -1)
1686 {
1687 byte index = (byte)(((Crc) & 0xFF) ^ b);
1688 Crc = ((Crc >> 8) ^ Table[index]);
1689 }
1690
1691 return ~Crc;
1692 }
1693
1694 private async Task<DataPort> BeginListen(int? Port, int Timeout)
1695 {
1696 bool ReleaseAfterUse = false;
1697
1698 if (!Port.HasValue)
1699 {
1700 if (this.Server.HasDataPorts)
1701 {
1702 Port = await this.Server.GetDataPort(Timeout);
1703 if (!Port.HasValue)
1704 return null;
1705
1706 ReleaseAfterUse = true;
1707 }
1708 }
1709
1710 try
1711 {
1712 DataPort Result = this.Server.OpenDataListener(
1713 this.AcceptTcpClientDataCallback, this.localEndpoint.Address,
1714 Port ?? 0, ReleaseAfterUse);
1715
1716 if (Result.LocalEndpoint is null)
1717 {
1718 Result.Listener.Stop();
1719 this.Server.Remove(Result);
1720
1721 return null;
1722 }
1723
1724 this.passiveStreams ??= new AsyncQueue<PassiveRecord>();
1725
1726 return Result;
1727 }
1728 catch (Exception ex)
1729 {
1730 if (ReleaseAfterUse)
1731 this.Server.ReleaseDataPort(Port.Value);
1732
1733 ExceptionDispatchInfo.Capture(ex).Throw();
1734 throw ex; // Not called, but avoids error about missing return.
1735 }
1736 }
1737
1738 private class PassiveRecord
1739 {
1740 public TaskCompletionSource<bool> PassiveChannel = new TaskCompletionSource<bool>();
1741 public DataPort PassivePort;
1742 public TcpClient PassiveClient;
1743 public bool DataConnectionEstablished = false;
1744 }
1745
1746 private void AcceptTcpClientDataCallback(IAsyncResult ar)
1747 {
1748 try
1749 {
1750 if (this.Server is null || this.Server.Disposed || NetworkingModule.Stopping)
1751 return;
1752
1753 DataPort Port = (DataPort)ar.AsyncState;
1754 TcpListener Listener = Port.Listener;
1755 TcpClient Client = null;
1756 bool Close = true;
1757
1758 try
1759 {
1760 Client = Listener.EndAcceptTcpClient(ar);
1761 Listener.Stop();
1762
1763 this.Information("Connection received on port " + Port.Port.ToString() +
1764 " from " + Client.Client.RemoteEndPoint.ToString());
1765
1766 if (!(Client.Client.RemoteEndPoint is IPEndPoint RemoteEndpoint) ||
1767 !RemoteEndpoint.Address.Equals(this.remoteEndpoint.Address))
1768 {
1769 this.Error("Connection discarded. Not from same remote endpoint.");
1770 }
1771 else
1772 {
1773 Close = false;
1774 Task _ = Task.Run(() => this.PassiveChannelEstablished(Port, Client));
1775 }
1776 }
1777 finally
1778 {
1779 if (Close)
1780 {
1781 if (!(Client is null))
1782 {
1783 this.Information("Closing data channel.");
1784 Client.Dispose();
1785 Port.Dispose();
1786 }
1787 }
1788 }
1789 }
1790 catch (Exception ex)
1791 {
1792 this.Exception(ex);
1793 }
1794 }
1795
1796 private async Task PassiveChannelEstablished(DataPort Port, TcpClient Client)
1797 {
1798 try
1799 {
1800 PassiveRecord Rec = await this.passiveStreams.Wait(this.cancelToken.Token, 10000);
1801 if (Rec is null)
1802 this.Warning("No passive stream available.");
1803 else
1804 {
1805 Rec.PassivePort = Port;
1806 Rec.PassiveClient = Client;
1807 Rec.PassiveChannel.TrySetResult(true);
1808
1809 Port = null;
1810 Client = null;
1811 }
1812 }
1813 catch (Exception ex)
1814 {
1815 this.Exception(ex);
1816 Client?.Dispose();
1817 }
1818 finally
1819 {
1820 Port?.Dispose();
1821 }
1822 }
1823
1824 private Task<bool> SendTextFile(string Content)
1825 {
1826 // TODO: Non UTF-8 encoding
1827 // TODO: Representation type
1828
1829 byte[] Bin = Encoding.UTF8.GetBytes(Content);
1830
1831 return this.SendBinaryFile(Bin, Content);
1832 }
1833
1834 private Task<bool> SendBinaryFile(byte[] Content)
1835 {
1836 return this.SendBinaryFile(Content, null);
1837 }
1838
1839 private Task<bool> SendBinaryFile(byte[] Content, string TextContent)
1840 {
1841 MemoryStream ms = new MemoryStream(Content);
1842 return this.SendBinaryFile(ms, TextContent, true);
1843 }
1844
1845 private Task<bool> SendBinaryFile(Stream Content, string TextContent, bool DisposeStream)
1846 {
1847 return this.TransferBinaryFile(Content, TextContent, DisposeStream, true, 0);
1848 }
1849
1850 private Task<bool> ReceiveBinaryFile(Stream Content, bool DisposeStream, long InitialSize)
1851 {
1852 return this.TransferBinaryFile(Content, null, DisposeStream, false, InitialSize);
1853 }
1854
1855 private async Task<bool> TransferBinaryFile(Stream Content, string TextContent,
1856 bool DisposeStream, bool Send, long InitialSize)
1857 {
1858 IPEndPoint EP;
1859 bool Connecting = false;
1860 TcpClient Client = null;
1861 DataPort PassivePort = null;
1862 BinaryTcpClient Client2 = null;
1863 string Message;
1864
1865 try
1866 {
1867 CancellationTokenSource Prev = this.cancelToken;
1868 this.cancelToken = new CancellationTokenSource();
1869 this.cancelled = new TaskCompletionSource<bool>();
1870 Prev?.Dispose();
1871
1872 if (this.ipExtension is null)
1873 {
1874 if (this.passiveStreams is null)
1875 return await this.BeginWrite("425 Unable to open data connection.\r\n", null, null);
1876
1877 if (this.protectionLevel == ProtectionLevel.Clear)
1878 Message = "150 Awaiting passive data connection.\r\n";
1879 else
1880 Message = "150 Awaiting passive encrypted data connection.\r\n";
1881
1882 if (!await this.BeginWrite(Message, null, null))
1883 return false;
1884
1885 PassiveRecord Rec = new PassiveRecord();
1886
1887 this.passiveStreams.Queue(Rec);
1888
1889 Task _ = Task.Delay(10000).ContinueWith((_) =>
1890 {
1891 if (!Rec.DataConnectionEstablished)
1892 Rec.PassiveChannel.TrySetResult(false);
1893
1894 return Task.CompletedTask;
1895 });
1896
1897 this.cancelToken.Token.Register(() =>
1898 {
1899 Rec.PassiveChannel.TrySetResult(false);
1900 });
1901
1902 if (!await Rec.PassiveChannel.Task)
1903 {
1904 if (this.cancelToken.IsCancellationRequested)
1905 {
1906 this.cancelled.TrySetResult(true);
1907 return await this.BeginWrite("426 Transfer aborted.\r\n", null, null);
1908 }
1909 else
1910 {
1911 this.cancelled.TrySetResult(false);
1912 return await this.BeginWrite("425 No data connection received.\r\n", null, null);
1913 }
1914 }
1915
1916 this.Information("Starting transfer on passive connection.");
1917
1918 PassivePort = Rec.PassivePort;
1919 Client = Rec.PassiveClient;
1920 }
1921 else
1922 {
1923 EP = this.ipExtension;
1924
1925 if (this.protectionLevel == ProtectionLevel.Clear)
1926 Message = "150 Opening data connection.\r\n";
1927 else
1928 Message = "150 Opening encrypted data connection.\r\n";
1929
1930 if (!await this.BeginWrite(Message, null, null))
1931 return false;
1932
1933 Connecting = true;
1934 Client = new TcpClient();
1935 await Client.ConnectAsync(EP.Address, EP.Port);
1936 }
1937
1938 Client2 = new BinaryTcpClient(Client, true, this.Sniffers);
1939 Client2.Bind(true);
1940
1941 if (this.IsEncrypted)
1942 {
1943 Client2.Information("Switching to TLS. (Client Certificates: " +
1944 this.clientCertificates.ToString() +
1945 ", Trust Certificates: " + this.trustCertificates.ToString() + ")");
1946
1947 await Client2.UpgradeToTlsAsServer(this.Server.ServerCertificate,
1948 Crypto.SecureTls, this.clientCertificates, null, this.trustCertificates,
1949 "ftp");
1950
1951 if (!(this.Client.RemoteCertificate is null) &&
1952 (Client2.RemoteCertificate is null ||
1953 this.Client.RemoteCertificate.GetPublicKeyString() !=
1954 Client2.RemoteCertificate.GetPublicKeyString()))
1955 {
1956 return await this.BeginWrite("425 Certificate on data channel did not match certificate on control channel.\r\n", null, null);
1957 }
1958 }
1959
1960 Connecting = false;
1961
1962 if (!string.IsNullOrEmpty(TextContent))
1963 {
1964 if (Send)
1965 this.TransmitText(TextContent);
1966 else
1967 this.ReceiveText(TextContent);
1968 }
1969
1970 if (!this.restartPoint.HasValue)
1971 Content.Position = Send ? 0 : Content.Length;
1972 else
1973 {
1974 long Pos = this.restartPoint.Value;
1975 this.restartPoint = null;
1976
1977 if (Pos > Content.Length)
1978 {
1979 this.restartPoint = null;
1980 return await this.BeginWrite("426 Restart point beyond EOF.\r\n", null, null);
1981 }
1982
1983 Content.Position = Pos;
1984 }
1985
1986 CopyResult Result;
1987
1988 if (Send)
1989 Result = await this.CopyAsync(Content, Client2.Stream, TextContent is null, false);
1990 else
1991 {
1992 Result = await this.CopyAsync(Client2.Stream, Content, true, true);
1993
1994 if (Result == CopyResult.Ok)
1995 {
1996 long NewSize = this.currentStorage + Content.Length - InitialSize;
1997
1998 if (this.maxStorage >= 0 && NewSize > this.maxStorage)
1999 Result = CopyResult.Overflow;
2000 else
2001 this.currentStorage = NewSize;
2002 }
2003 }
2004
2005 if (Result != CopyResult.Ok && DisposeStream && !Send && Content is FileStream fs)
2006 {
2007 string Name = fs.Name;
2008
2009 fs.Dispose();
2010 DisposeStream = false;
2011
2012 if (File.Exists(Name))
2013 {
2014 File.Delete(Name);
2015 this.currentStorage -= InitialSize;
2016 }
2017 }
2018
2019 switch (Result)
2020 {
2021 case CopyResult.Failed:
2022 return await this.BeginWrite("426 Transfer failed.\r\n", null, null);
2023
2024 case CopyResult.Aborted:
2025 return await this.BeginWrite("426 Transfer aborted.\r\n", null, null);
2026
2027 case CopyResult.UnexpectedEnd:
2028 return await this.BeginWrite("426 Unexpected end of file.\r\n", null, null);
2029
2030 case CopyResult.Overflow:
2031 return await this.BeginWrite("552 Exceeded storage allocation.\r\n", null, null);
2032
2033 default:
2034 case CopyResult.Ok:
2035 await Client2.Stream.FlushAsync();
2036 return await this.BeginWrite("226 Transfer complete.\r\n", null, null);
2037 }
2038 }
2039 catch (Exception ex)
2040 {
2041 this.Exception(ex);
2042
2043 if (!(Client2 is null) && ex is AuthenticationException)
2044 await this.Server.LoginFailure(ex, Client2, Client2.RemoteEndPoint);
2045
2046 if (Connecting)
2047 return await this.BeginWrite("425 Unable to open connection: " + FirstRow(ex) + ".\r\n", null, null);
2048 else
2049 return await this.BeginWrite("425 Unable to transfer file: " + FirstRow(ex) + ".\r\n", null, null);
2050 }
2051 finally
2052 {
2053 try
2054 {
2055 PassivePort?.Dispose();
2056
2057 if (DisposeStream)
2058 {
2059 await Content.FlushAsync();
2060 Content.Dispose();
2061 }
2062
2063 if (Client2 is null)
2064 Client?.Dispose();
2065 else
2066 {
2067 await Client2.RemoveRange(this.Sniffers, false);
2068 await Client2.DisposeAsync();
2069 }
2070 }
2071 catch (Exception ex)
2072 {
2073 Log.Exception(ex);
2074 }
2075 }
2076 }
2077
2078 private enum CopyResult
2079 {
2080 Ok,
2081 Failed,
2082 UnexpectedEnd,
2083 Aborted,
2084 Overflow
2085 }
2086
2087 private async Task<CopyResult> CopyAsync(Stream From, Stream To, bool SniffBinary,
2088 bool UntilClose)
2089 {
2090 try
2091 {
2092 CancellationTokenSource Cancel = this.cancelToken;
2093 long BytesLeft = UntilClose ? long.MaxValue : From.Length - From.Position;
2094 int BufSize = (int)Math.Min(65536, BytesLeft);
2095 byte[] Bin = new byte[BufSize];
2096 int NrRead;
2097
2098 while (UntilClose || BytesLeft > 0)
2099 {
2100 NrRead = await From.ReadAsync(Bin, 0, (int)Math.Min(BufSize, BytesLeft));
2101 if (NrRead <= 0)
2102 {
2103 if (UntilClose)
2104 return CopyResult.Ok;
2105 else
2106 {
2107 this.Error("Unexpected end of file.");
2108 return CopyResult.UnexpectedEnd;
2109 }
2110 }
2111
2112 if (SniffBinary)
2113 {
2114 if (UntilClose)
2115 this.ReceiveBinary(NrRead);
2116 else
2117 this.TransmitBinary(NrRead);
2118 }
2119
2120 await To.WriteAsync(Bin, 0, NrRead, Cancel.Token);
2121
2122 if (!UntilClose)
2123 BytesLeft -= NrRead;
2124
2125 if (Cancel.IsCancellationRequested)
2126 {
2127 this.cancelled.TrySetResult(true);
2128 return CopyResult.Aborted;
2129 }
2130 }
2131
2132 return CopyResult.Ok;
2133 }
2134 catch (Exception ex)
2135 {
2136 this.Exception(ex);
2137 return CopyResult.Failed;
2138 }
2139 }
2140
2141 private static string FirstRow(Exception ex)
2142 {
2143 ex = Log.UnnestException(ex);
2144
2145 string s = ex.Message;
2146
2147 int i = s.IndexOfAny(CRLF);
2148 if (i < 0)
2149 return s;
2150 else
2151 return s[..i];
2152 }
2153
2154 private static readonly char[] CRLF = new char[] { '\r', '\n' };
2155
2156 private static string ReadPrivilege(string PathName)
2157 {
2158 return FtpPrivilege(PathName, FtpServer.FtpReadPrivilegePrefix);
2159 }
2160
2161 private static string FtpPrivilege(string PathName, string Prefix)
2162 {
2163 PathName = Path.GetFullPath(PathName);
2164 string Result = PathName.Replace(':', '.').Replace('/', '.').Replace('\\', '.');
2165
2166 while (Result.Contains(".."))
2167 Result = Result.Replace("..", ".");
2168
2169 return Prefix + Result;
2170 }
2171
2172 private static string WritePrivilege(string PathName)
2173 {
2174 return FtpPrivilege(PathName, FtpServer.FtpWritePrivilegePrefix);
2175 }
2176
2177 private async Task<string> CanLogin()
2178 {
2179 LoginAuditor Auditor = this.Server.PersistenceLayer.Auditor;
2180 DateTime? Next;
2181
2182 if (Auditor is null)
2183 Next = null;
2184 else
2185 Next = await Auditor.GetEarliestLoginOpportunity(this.RemoteEndPoint, "FTP");
2186
2187 if (Next.HasValue)
2188 {
2189 StringBuilder sb = new StringBuilder();
2190 DateTime TP = Next.Value;
2191 DateTime Today = DateTime.Today;
2192
2193 sb.Append("530 ");
2194
2195 if (Next.Value == DateTime.MaxValue)
2196 {
2197 sb.Append("This endpoint (");
2198 sb.Append(this.RemoteEndPoint);
2199 sb.Append(") has been blocked from the system");
2200 }
2201 else
2202 {
2203 sb.Append("Too many failed login attempts in a row registered. Try again after ");
2204 sb.Append(TP.ToLongTimeString());
2205
2206 if (TP.Date != Today)
2207 {
2208 if (TP.Date == Today.AddDays(1))
2209 sb.Append(" tomorrow");
2210 else
2211 {
2212 sb.Append(", ");
2213 sb.Append(TP.ToShortDateString());
2214 }
2215 }
2216 }
2217
2218 sb.Append(". Remote Endpoint: ");
2219 sb.Append(this.RemoteEndPoint);
2220 sb.Append("\r\n");
2221
2222 return sb.ToString();
2223 }
2224
2225 return null;
2226 }
2227
2232 protected override async Task<object> BeforeUpgradeToTls()
2233 {
2234 FtpControlConnectionState Result = this.state;
2235 await this.SetState(FtpControlConnectionState.StartingEncryption);
2236 return Result;
2237 }
2238
2243 protected override async Task AfterUpgradeToTls(object Item)
2244 {
2245 if (Item is FtpControlConnectionState State)
2246 await this.SetState(State);
2247 }
2248
2253 public override async Task SetAccount(IAccount Account)
2254 {
2255 await base.SetAccount(Account);
2256 await this.SetState(FtpControlConnectionState.Authenticated);
2257
2258 this.maxStorage = await this.Server.PersistenceLayer.GetMaxStorage(Account.UserName);
2259 this.currentStorage = CalcStorage(this.rootFolder);
2260 }
2261
2262 private static long CalcStorage(string Folder)
2263 {
2264 long Result = 0;
2265
2266 if (Directory.Exists(Folder))
2267 {
2268 foreach (string FileName in Directory.GetFiles(Folder))
2269 {
2270 FileInfo Info = new FileInfo(FileName);
2271 Result += Info.Length;
2272 }
2273
2274 foreach (string SubFolder in Directory.GetDirectories(Folder))
2275 Result += CalcStorage(SubFolder);
2276 }
2277
2278 return Result;
2279 }
2280
2285 public override bool CheckLive()
2286 {
2287 if (this.state == FtpControlConnectionState.Error || this.state == FtpControlConnectionState.Offline)
2288 return false;
2289 else
2290 return base.CheckLive();
2291 }
2292
2297 public override void ResetState(bool Authenticated)
2298 {
2299 this.user = string.Empty;
2300 this.rootFolder = string.Empty;
2301 this.localFolder = string.Empty;
2302 this.protectionLevel = ProtectionLevel.Clear;
2303 this.type = RepresentationType.AsciiNonPrint;
2304 this.ipExtension = null;
2305 this.utf8 = false;
2306 this.onlyPassive = false;
2307
2308 base.ResetState(Authenticated);
2309 }
2310
2315 public override Task<bool> SaslErrorNotAuthorized()
2316 {
2317 return this.BeginWrite("530 Not logged in.\r\n", null, null);
2318 }
2319
2324 public override Task<bool> SaslErrorAccountDisabled()
2325 {
2326 return this.BeginWrite("530 Account disabled.\r\n", null, null);
2327 }
2328
2333 public override Task<bool> SaslErrorMalformedRequest()
2334 {
2335 return this.BeginWrite("530 Malformed request.\r\n", null, null);
2336 }
2337
2343 public override Task<bool> SaslChallenge(string ChallengeBase64)
2344 {
2345 // TODO: Correct code
2346 if (string.IsNullOrEmpty(ChallengeBase64))
2347 return this.BeginWrite("334\r\n", null, null);
2348 else
2349 return this.BeginWrite("334 " + ChallengeBase64 + "\r\n", null, null);
2350 }
2351
2357 public override Task<bool> SaslSuccess(string ProofBase64)
2358 {
2359 return this.BeginWrite("230 User logged in, proceed.\r\n", null, null);
2360 }
2361
2362 }
2363}
Helps with parsing of commong data types.
Definition: CommonTypes.cs:15
static bool TryParse(string s, out double Value)
Tries to decode a string encoded double.
Definition: CommonTypes.cs:48
Static class managing encoding and decoding of internet content.
static string GetContentType(string FileExtension)
Gets the content type of an item, given its file extension. It uses the TryGetContentType to see if a...
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
static Exception UnnestException(Exception Exception)
Unnests an exception, to extract the relevant inner exception.
Definition: Log.cs:828
Implements a binary TCP Client, by encapsulating a TcpClient. It also makes the use of TcpClient safe...
void Bind()
Binds to a TcpClient that was already connected when provided to the constructor.
TcpClient Client
Underlying TcpClient object.
string RemoteEndPoint
Remote End-point of connection. This corresponds to the IP Endpoint of the remote party in normal cas...
Stream Stream
Stream object currently being used.
virtual Task DisposeAsync()
Disposes of the object asynchronously. The underlying TcpClient is either disposed directly,...
void Dispose()
Disposes of the object. The underlying TcpClient is either disposed directly, or when asynchronous op...
static string GetDomainFromSubject(string Subject)
Extracts the domain name from a certificate subject string.
X509Certificate RemoteCertificate
Certificate used by the remote endpoint.
Task UpgradeToTlsAsServer(X509Certificate ServerCertificate)
Upgrades a server connection to TLS.
bool IsEncrypted
If connection is encrypted or not.
void Exception(Exception Exception)
Called to inform the viewer of an exception state.
void ReceiveText(string Text)
Called when text has been received.
ISniffer[] Sniffers
Registered sniffers.
void Error(string Error)
Called to inform the viewer of an error state.
void Information(string Comment)
Called to inform the viewer of something.
Task RemoveRange(IEnumerable< ISniffer > Sniffers)
Removes a set of sniffers, if registered.
Abstract base class for FTP client connections.
bool IsEncrypted
If the connection is encrypted.
IAccount Account
Account of authenticated user.
Task< bool > BeginWrite(string Text, EventHandlerAsync< DeliveryEventArgs > Callback, object State)
Starts sending a text command to the client.
void ResetState()
Resets the state of the connection.
BinaryTcpClient Client
Underlying TCP connection.
FtpServer Server
FTP Server serving the client.
EventHandlerAsync< FtpControlConnectionState > OnStateChanged
Event raised whenever the internal state of the connection changes.
FtpControlConnectionState State
Current state of connection.
FtpClientControlConnection(BinaryTcpClient Client, FtpServer Server, IFtpServerPersistenceLayer Persistence, ClientCertificates ClientCertificates, bool TrustCertificates, params ISniffer[] Sniffers)
Class managing an FTP control connection.
override Task< bool > SaslErrorMalformedRequest()
Is called when a an authentication attempt has been made using a malformed request.
override async Task ErrorAndClose()
Closes the connection due to an error.
override void ResetState(bool Authenticated)
Resets the state of the connection.
override bool CheckLive()
Checks if the connection is live.
override Task< bool > SaslErrorNotAuthorized()
Is called when a failed authentication attempt has been made.
override async Task SetAccount(IAccount Account)
Sets the account for the connection.
override async Task AfterUpgradeToTls(object Item)
Called after upgrading to TLS.
async override Task< bool > ParseIncoming(bool ConstantBuffer, byte[] Data, int Offset, int NrRead)
Parses incoming binary data.
override Task< bool > SaslChallenge(string ChallengeBase64)
Is called when a an authentication challenge has been received.
override Task< bool > SaslSuccess(string ProofBase64)
Is called when a a successful authentication response has been received.
override Task< bool > SaslErrorAccountDisabled()
Is called when a an authentication attempt has been made using a disabled account.
override async Task< object > BeforeUpgradeToTls()
Called before upgrading to TLS.
async override Task DisposeAsync()
Closes the connection and disposes of all resources.
Implements a simple FTP Server, as defined in:
Definition: FtpServer.cs:38
bool AllowSafeDataChannel
If data can be transferred in the clear, with integrity protection
Definition: FtpServer.cs:434
bool AllowConfidentialDataChannel
If data can be transferred encrypted.
Definition: FtpServer.cs:439
bool AllowPrivateDataChannel
If data can be transferred encrypted, with integrity protection.
Definition: FtpServer.cs:444
bool HasDataPorts
If FTP Server has data ports defined.
Definition: FtpServer.cs:332
bool Disposed
If the class is disposed.
Definition: FtpServer.cs:424
bool AllowClearDataChannel
If data can be transferred in the clear
Definition: FtpServer.cs:429
void ReleaseDataPort(int Port)
Releases a port back to the pool of available data ports.
Definition: FtpServer.cs:354
async Task< int?> GetDataPort(int Timeout)
Gets a free data port for use in a passive-mode data connection.
Definition: FtpServer.cs:340
bool EncryptionRequired
If C2S encryption is requried.
Definition: FtpServer.cs:523
Module that controls the life cycle of communication.
static bool Stopping
If the system is stopping.
Asynchronous First-in-First-out (FIFO) Queue, for use when transporting items of type T between task...
Definition: AsyncQueue.cs:16
Task< T > Wait()
Waits indefinitely (or until queue is disposed) for an item to be available. If Queue is disposed,...
Definition: AsyncQueue.cs:327
void Queue(T Item)
Queues an item for processing by adding it last in the queue. No information is returned wether the i...
Definition: AsyncQueue.cs:160
void Dispose()
IDisposable.Dispose
Definition: AsyncQueue.cs:539
Helper methods for encrypting and decrypting streams of data.
Definition: Crypto.cs:14
const SslProtocols SecureTls
TLS 1.2 & 1.3
Definition: Crypto.cs:18
Contains methods for simple hash calculations.
Definition: Hashes.cs:57
static string ComputeSHA512HashString(byte[] Data)
Computes the SHA-512 hash of a block of binary data.
Definition: Hashes.cs:557
static string ComputeSHA256HashString(byte[] Data)
Computes the SHA-256 hash of a block of binary data.
Definition: Hashes.cs:449
static string ComputeSHA1HashString(byte[] Data)
Computes the SHA-1 hash of a block of binary data.
Definition: Hashes.cs:395
static string ComputeMD5HashString(byte[] Data)
Computes the MD5 hash of a block of binary data.
Definition: Hashes.cs:611
Class that monitors login events, and help applications determine malicious intent....
Definition: LoginAuditor.cs:26
async Task< DateTime?> GetEarliestLoginOpportunity(string RemoteEndPoint, string Protocol)
Checks when a remote endpoint can login.
static async void Success(string Message, string UserName, string RemoteEndPoint, string Protocol, params KeyValuePair< string, object >[] Tags)
Handles a successful login attempt.
static void Fail(string Message, string UserName, string RemoteEndPoint, string Protocol)
Handles a failed login attempt.
Login state information relating to a remote endpoint
Task< string > GetRootFolder(string UserName)
Gets the root folder of a user.
Interface for SMTP user accounts.
Definition: IAccount.cs:11
CaseInsensitiveString UserName
User Name
Definition: IAccount.cs:24
bool Enabled
If the account is enabled.
Definition: IAccount.cs:40
bool HasPrivilege(string PrivilegeID)
If the account has a given privilege.
Task< IAccount > GetAccount(CaseInsensitiveString UserName)
Method to call to fetch account information.
Interface for sniffers. Sniffers can be added to ICommunicationLayer classes to eavesdrop on communic...
Definition: ISniffer.cs:10
Definition: ImplTypes.g.cs:58
delegate string ToString(IElement Element)
Delegate for callback methods that convert an element value to a string.
Overflow
Overflow handling.
Definition: LayoutArea.cs:11
FtpControlConnectionState
State of FTP connection.
ProtectionLevel
Data Channel Protection Level
TransferMode
Data Transfer Mode
Definition: TransferMode.cs:7
RepresentationType
Representation type of files.
ClientCertificates
Client Certificate Options
Prefix
SI prefixes. http://physics.nist.gov/cuu/Units/prefixes.html
Definition: Prefixes.cs:11