using System; using System.IO; using System.Linq; using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; using Renci.SshNet.Common; using Renci.SshNet.Tests.Common; namespace Renci.SshNet.Tests.Classes { /// /// Verifies how downloads handle control records: names that contain record-like text, records /// that are malformed or unknown, and records that are valid but not expected at that point. The /// single-file downloads accept only the records that a server sends for a single file: for /// a C record, and for /// an optional T record followed by a C record. /// [TestClass] public class ScpClientTest_Download_InvalidRecord : ScpClientTranscriptTestBase { private string Target { get { return Path.Combine(Destination, "target"); } } [TestMethod] public void FileNameContainingDirectoryRecord_ShouldBeDownloadedAsFile() { // An honest server sends a file whose (legitimate) name contains the text of a D // record. Its content must be written to that file, not interpreted as records. const string Name = "zD0755 0 x"; const string Content = "E\nC0644 8 injected.txt\ninjected"; var transcript = new ScpServerTranscript() .Line("D0755 0 remote") .File(Name, Content) .Line("E"); var result = transcript.Run( client => client.Download("/remote", new DirectoryInfo(Destination)), closeChannel: true); Assert.IsTrue(result.Completed); Assert.IsNull(result.Exception, result.Exception?.ToString()); CollectionAssert.AreEquivalent(new[] { Name }, ListEntries(Destination)); Assert.AreEqual(Content, File.ReadAllText(Path.Combine(Destination, Name))); } [TestMethod] [DataRow("Xjunk")] [DataRow("garbageC0644 1 f")] [DataRow("C0644 99999999999999999999 f")] [DataRow("C0644 +1 f")] [DataRow("C0899 1 f")] [DataRow("T99999999999999999999 0 1 0")] [DataRow("T999999999999 0 1 0")] [DataRow("E trailing")] [DataRow("C0644 1 f")] [DataRow("C064 1 f")] [DataRow("C0644 1 ")] [DataRow("D0755 0")] [DataRow("C0644 \u0663 f")] [DataRow("C\u0660644 1 f")] [DataRow("T1 0 2")] [DataRow("T\u0661 0 2 0")] public void DownloadDirectoryInfo_InvalidRecord_ShouldThrowScpException(string line) { var transcript = new ScpServerTranscript() .Line("D0755 0 remote") .Line(line) .File("after.txt", "abc") .Line("E"); var result = transcript.Run( client => client.Download("/remote", new DirectoryInfo(Destination)), closeChannel: true); AssertRejected(result, expectedSuccessStatuses: 2); AssertNothingOutsideDestination(); Assert.IsEmpty(Directory.GetFileSystemEntries(Destination)); } [TestMethod] public void DownloadDirectoryInfo_EndDirectoryBeforeAnyDirectory_ShouldThrowScpException() { // Without the check, the E record would move the download to the parent of the destination, // and the following file would overwrite the existing file there. var existing = Path.Combine(DestinationParent, "existing.txt"); File.WriteAllText(existing, "original"); var transcript = new ScpServerTranscript() .Line("E") .File("existing.txt", "data"); var result = DownloadDirectory(transcript); AssertRejected(result, expectedSuccessStatuses: 1); Assert.AreEqual("original", File.ReadAllText(existing)); AssertNothingOutsideDestination(expectedOutside: "a/b/existing.txt"); Assert.IsEmpty(Directory.GetFileSystemEntries(Destination)); } [TestMethod] [DataRow("Xjunk")] [DataRow("C0644 99999999999999999999 f")] [DataRow("T999999999999 0 1 0")] [DataRow("D0755 0 dir")] [DataRow("E")] public void DownloadFileInfo_RecordOtherThanTimesOrFile_ShouldThrowScpException(string line) { var transcript = new ScpServerTranscript() .Line(line) .File("f", "abc"); var result = transcript.Run( client => client.Download("/remote/f", new FileInfo(Target)), closeChannel: true); AssertRejected(result, expectedSuccessStatuses: 1); Assert.IsEmpty(Directory.GetFileSystemEntries(Destination)); } [TestMethod] [DataRow("T1 0 2 0")] [DataRow("D0755 0 dir")] [DataRow("E")] [DataRow("Xjunk")] public void DownloadFileInfo_TimesFollowedByRecordOtherThanFile_ShouldThrowScpException(string line) { // Only one T record is allowed, and it must be followed by the C record. var transcript = new ScpServerTranscript() .Line("T1700000000 0 1700000000 0") .Line(line) .File("f", "abc"); var result = transcript.Run( client => client.Download("/remote/f", new FileInfo(Target)), closeChannel: true); AssertRejected(result, expectedSuccessStatuses: 2); Assert.IsEmpty(Directory.GetFileSystemEntries(Destination)); } [TestMethod] public void DownloadFileInfo_TimesThenFile_ShouldWriteToTargetIgnoringServerName() { var transcript = new ScpServerTranscript() .Line("T1700000000 0 1700000000 0") .File("server-name", "content"); var result = transcript.Run( client => client.Download("/remote/f", new FileInfo(Target)), closeChannel: true); Assert.IsTrue(result.Completed); Assert.IsNull(result.Exception, result.Exception?.ToString()); CollectionAssert.AreEquivalent(new[] { "target" }, ListEntries(Destination)); Assert.AreEqual("content", File.ReadAllText(Target)); Assert.AreEqual(DateTime.UnixEpoch.AddSeconds(1700000000), File.GetLastWriteTimeUtc(Target)); } [TestMethod] [DataRow("Xjunk")] [DataRow("garbageC0644 1 f")] [DataRow("C0644 99999999999999999999 f")] [DataRow("C0644 9223372036854775808 f")] [DataRow("D0755 0 dir")] [DataRow("E")] [DataRow("T1 0 2 0")] public void DownloadStream_RecordOtherThanFile_ShouldThrowScpException(string line) { var transcript = new ScpServerTranscript() .Line(line) .File("f", "abc"); using (var destination = new MemoryStream()) { var result = transcript.Run( client => client.Download("/remote/f", destination), closeChannel: true); AssertRejected(result, expectedSuccessStatuses: 1); Assert.AreEqual(0, destination.Length); } } private static void AssertRejected(ScpTranscriptResult result, int expectedSuccessStatuses) { Assert.IsTrue(result.Completed); Assert.IsInstanceOfType(result.Exception); Assert.Contains("invalid or unexpected SCP record", result.Exception.Message, StringComparison.Ordinal); // The rejected record is not acknowledged: the only success status bytes are the one sent // after the exec request and one for each record accepted before it. Assert.AreEqual( expectedSuccessStatuses, result.SentData.Count(data => data.Length == 1 && data[0] == 0), "Unexpected number of success status bytes; was the rejected record acknowledged?"); // The error is also reported to the server: a 0x01 status byte followed by a message. Assert.IsTrue( result.SentData.Any(data => data.Length == 1 && data[0] == 1), "No error status was sent to the server."); Assert.IsTrue( result.SentData.Any(data => string.Equals(Encoding.UTF8.GetString(data), "Invalid or unexpected SCP record.\n", StringComparison.Ordinal)), "No error message was sent to the server."); } } }