Telnet 文件浏览器






4.93/5 (8投票s)
一个简单的文件浏览器和通过 telnet 连接进行文件传输的工具
引言
警告:此代码旨在操作远程系统上的文件。在使用前,请务必了解您的操作!
---
出于纯粹的好奇心,我一直在玩弄家里的 AVM Fritz!Box,了解它除了作为路由器之外还能做什么。您可以通过从连接到 Fritz!Box 的电话拨打 #96*7* 来激活 Telnet 服务器(#96*8* 来禁用)。于是,我开始查看文件系统,发现了很多 LUA 脚本,我想仔细研究一下。但仅仅使用 cat
命令,然后在 PuTTY 控制台中滚动浏览行,并不是很方便,所以我一直在寻找一种方法将这些文件复制到我的电脑上,因为 Notepad++ 支持 LUA 脚本的语法高亮,并且可以更轻松地同时处理不同的文件。另外,使用 cd
和 ls
在目录树中导航,作为 Windows 用户,我并不太满意;b
我开始在 Google 上搜索 Telnet 文件浏览器,但没找到——好吧,说实话,我并没有真正深入搜索,因为我喜欢这种挑战。于是,我开始编写这样一个浏览器,这个项目专门用于 Fritz!Box,但应该可以在稍作修改的情况下用于任何 Telnet 服务器。
---
背景
远程端:基本的 Linux 命令
AVM Fritz!Box 使用类似 *nix 的 BusyBox 系统,其中包含许多标准命令(有关完整列表,请参阅 此 德语页面)。我不太熟悉使用 *nix 类系统,尽管我很想,所以让我们来看看我在远程 Telnet 机器上使用的命令——如果您熟悉这些,欢迎 跳过
命令 'LS'
ls [-1AacCdeFilnpLRrSsTtuvwxXhk] [{filenames}]
在 Windows 中您使用 dir
,而在 *nix 系统中您总是使用 ls
。有很多命令行开关,但我只会解释我将使用的那些。
参数 | 描述 |
---|---|
-a |
显示隐藏条目(以点 '.' 开头) |
-l |
显示详细列表,每行一个条目 |
-e |
显示完整的时戳,包含星期几、秒和年份 |
-R |
递归列出所有子目录 |
{文件名} |
一个起始目录和/或一个用于列表的过滤器 |
为了获取我想要的所有信息,我使用所有这些参数,如下所示:
ls -a -l -e -R /
这将产生如下输出:
./:
drwxr-x--- 0 root root 271 Fri Feb 7 17:17:59 2014 mount
drwxr-x--- 3 root root 154 Fri Feb 7 17:17:59 2014 home
-rwxrwx--- 0 root root 198271 Fri Feb 7 17:17:59 2014 somefile.txt
./mount:
./home:
drwxr-x--- 1 root root 154 Fri Feb 7 17:17:59 2014 mysubfolder
-rwxr-x--- 0 root root 78651 Fri Feb 7 17:17:59 2014 readme.txt
lrwxr-x--- 0 root root 0 Fri Feb 7 17:17:59 2014 link -> ./somefile.txt
./home/mysubfolder:
-r-xr-x--- 2 root root 2328156 Fri Feb 7 17:17:59 2014 anotherfile.dat
请注意,只有当我使用根目录作为起始目录时,我才在路径描述(以冒号 ':' 结尾的那些)之前发现了这些点 '.'——否则它们以斜杠 '/' 开头。这里列出的每个文件或文件夹的信息是:
零件 | 描述 |
---|---|
第 1 个字符,第 1 块 |
它是 'd' 表示目录,'l' 表示链接,或者 '-' 表示普通文件。 |
第 1 块的其余部分 |
权限:3 次 'rwx' 分别表示 'r'=读取,'w'=写入,'x'=执行;如果不允许,则在该位置显示破折号 '-'。第一个 'rwx' 适用于 root 用户,第二个适用于当前组,第三个适用于所有用户。 |
第 2 块 |
子条目数 |
第 3 块 |
Owner |
第 4 块 |
组 |
第 5 块 |
大小(字节) |
第 6 块至第 10 块 |
修改日期和时间 |
第 11 块 |
文件或目录名 |
第 12 块和第 13 块 |
如果存在,则为链接目标 |
在客户端,我会在启动时简单地捕获整个树的快照。这需要一些时间,但比以后动态加载单个部分更不容易出错。
命令 'CAT'
cat {filename}
它只是将文件的内容输出到控制台。尽管此方法可能会稍微改变输出,但在 Fritz!Box 上,如果没有额外的连接,这是获取文件内容的唯一方法,因为它没有 hexdump
命令或等效命令来转换不可打印字符。此外,换行符总是会被转换为当前的新行标准,在我的情况下是 <CR><LF>,即使原始文件只使用 <LF>。对于文本文件来说,这关系不大,但以这种方式加载二进制文件是不可能的。
命令 'NC'
nc [{addr} {port}|-l -p {port}] [<|>] {filename}
'NC' 表示 netcat,所以它是网络上的 cat。这不是 netcat 的全部功能,但这是我打算使用的。它使用一个单独的连接来发送或接收数据,同时您可以将文件设置为输入或输出。以下是命令行开关的含义:
参数 | 描述 |
---|---|
{地址} {端口} |
定义要连接的目标地址和端口 |
-l -p {端口} |
将 NC 启动为监听器,等待端口上的连接 |
< {文件名} |
定义一个输入文件,其内容将在建立连接后立即发送 |
> {文件名} |
定义一个输出文件,用于在建立连接后将接收到的数据写入其中 |
通常有两种交换文件的方式:
- 客户端->服务器:服务器打开一个端口,客户端连接到它。
- 客户端<-服务器:客户端打开一个端口,服务器连接到它。
无论是发送文件还是接收文件,这都不重要——重要的是两个系统所处的环境。取决于哪一方更容易打开一个可访问的端口,那一方面就应该作为主机打开端口。无论哪种方式,文件都可以双向传输。
以下是两个从服务器复制文件到客户端的示例(首先是服务器,然后是客户端监听):
nc -l -p 4322 < /home/afile.txt
nc 192.168.178.20 4321 < /home/afile.txt
以及两个从客户端复制文件到服务器的类似示例:
nc -l -p 4322 > /home/newfile.txt
nc 192.168.178.20 4321 > /home/newfile.txt
命令 'ECHO'
此命令通常用于在脚本中将文本行输出到屏幕,但如果重定向,则可用于创建文本文件。带重定向的语法是:
echo -n $'{some text}' [>|>>] {filename}
参数 -n
可防止 echo 命令附加尾随的换行符。
但是,在某些情况下,文件数据需要进行转义。我将使用严格的引用——文本将 enclosed 如下 $'{some text}'
,并且为了兼容性,我将转义大多数字符。
- 零字符(0x00)无法写入,因为它会终止字符串。
- 值小于 0x20 的所有字符都是控制字符,将使用八进制转义格式进行转义:
0xnnn
,其中 nnn 是该字符的 3 位八进制值。 - 值大于 0x7F 的所有字符都是扩展 ASCII 字符,也将使用八进制转义格式进行转义,如上所示。
- 所有 '单引号'(0x27)字符显然必须转义,因为它们否则会结束引号,所以我将使用
\'
进行直接转义。 - 反斜杠字符(0x5C)将直接使用
\\
进行转义。 - 所有其他字符将按原样传输。
通过这种方式,我不必逐行发送文件,而是可以将其拆分成几部分。尽管大多数 *nix 机器不像 Windows 那样有 8191 个字符(或旧版本为 2047 个)的快速命令行长度限制,但我认为一次性发送所有内容而不是将其分块发送大约 1000 个字符的段是一个好主意。这是一个例子:
echo -n $'<html>\012<head><title>\\\'</title></head>\012<body></body>\012</html>' > n.txt
这将创建一个包含四行文本的 n.txt 文件:
<html>
<head><title>\'</title></head>
<body></body>
</html>
此方法(像 cat 一样)当然只能用于文本文件,因为可能存在除了 0x00 之外的其他字符无法按预期写入。
命令 'MKDIR'
此命令创建一个新目录。如果使用命令行选项 -p
,所有不存在的父目录也会被创建。这是一个例子:
mkdir -p /var/tmp/newfolder/andsubfolder
命令 'RM'
此命令可以删除(移除)文件或目录树。如果使用命令行选项 -f,则不会有确认提示;如果使用 -r,则会递归删除目录及其所有内容。这里有两个例子——第一个是删除一个文件,然后是删除一个目录树:
rm -f /usr/file.tmp
rm -f -r /var/tmp/my_tmp_folder
---
客户端
通用代码结构
我对客户端这边比较熟悉,因为 C#.NET 是我熟悉的。首先,我将解释我的代码结构。这是类的连接方式:
TelnetFileInfo
类是一个简单的数据类,用于存储文件信息。TelnetDirectoryInfo
类继承了 TelnetFileInfo 类,添加了一个 TelnetDirectoryInfo 列表作为 SubDirectories 和一个 TelnetFileInfo 列表作为 Files,以及一个用于加载具有指定起始目录的完整文件树的方法。此类将存储一个目录/文件树。要加载完整树,您需要传递一个 TelnetConnection 实例——这就是为什么它被标记为“使用 TelnetConnection”。TelnetConnection
类是此项目的核心,因为它处理大部分与 telnet 通信相关的逻辑。对于使用 netcat 发送和接收文件,它使用 TcpConnectionHelper 类来处理连接或监听的第二个连接。- 静态类
TcpConnectionHelper
是一个助手,用于创建连接或监听连接,并在连接后自动执行数据传输。
通用工作流程
我设计了一个简单的表单,左侧有一个 TreeView
,右侧有一个 ListView
。在 ListView 下方,我添加了一些控件来启动上传和下载。此外,我还设计了一个简单的窗口来显示状态信息,因为下载需要一些时间,所以它在一个线程中运行,用户可以看到正在进行操作。
TreeView
将显示加载后的目录树。有三种不同类型的条目,它们具有不同的图标和颜色来表示它们:
- 目录 以红色显示,用括号括起来,并带有文件夹图标。
- 文件 以黑色显示,带有通用的文档图标。
- 链接 以蓝色显示,带有带有绿色箭头的空纸张图标。请注意,链接可以指向文件或目录,这不区分。
ListView
将简单地显示当前选定条目的信息。
“下载方法”下拉菜单(我将将其重命名为“传输方法”)设置了以下三种方法之一来上传或下载文件(或目录):
- NETCAT 客户端 >> 服务器:使用第二个连接连接到指定端口上的服务器,使用服务器上的 NETCAT 来接收或发送文件。
- NETCAT 客户端 << 服务器:使用第二个连接监听指定端口,直到服务器连接到它并发送或接收文件。
- CAT/ECHO(仅限文本文件!!!):使用现有连接通过控制台输入或输出来发送或接收文件。如上所述,这不适用于二进制文件,但万一环境不允许客户端或服务器打开另一个监听端口,此方法可以用作备用方法,至少传输文本文件。
使用该应用程序,您应该执行以下步骤:
- 在密码文本框中输入您的密码。
- 按 [Enter] 或单击连接按钮,然后等待目录树加载。
- 下载:选择一个文件或目录,在“* 端口:”文本框中输入一个合适的端口,然后单击“下载”按钮,选择一个目标,单击“确定”按钮,然后等待操作完成。
- 上传:选择一个目录,在“* 端口:”文本框中输入一个合适的端口,然后单击“上传”按钮,选择要上传的文件,单击“确定”按钮,然后等待操作完成。目前只能将文件上传到现有目录。
请记住,几乎没有错误处理或输入检查,所以请谨慎操作!
---
使用代码
我将从下往上解释代码的使用。
TcpConnectionHelper
ConnectToClient
和 WaitForConnection
方法是内部使用的。我只将它们设为 public,以便有可能使用自定义操作。代码内部有如何使用的描述。
所有 SendData*
和 ReceiveData*
方法都使用类似的模式,因为它们只是彼此的不同组合和包装器。它们也在代码中有很好的文档记录。这些方法设计为作为远程系统上启动的 netcat 的终结点。因为 netcat 不会自动终止连接(除非设置超时,而我没有尝试),所以我设计了这些方法,在没有数据传输的情况下,在恒定超时后终止连接。所有以 *Connecting
结尾的方法都在内部使用 ConnectToClient 方法,并主动连接到参数中指定的地址和端口。所有以 *Listening
结尾的方法都在内部使用 WaitForConnection 方法,并等待远程连接到参数中指定的本地端口。在任何一种情况下,您都可以使用字节数组、文件路径或 Stream 来传输数据。所有使用字节数组或文件路径作为参数(或返回值)的方法都是相应方法使用 Stream 的简单包装器。以下示例展示了如何将一个图像文件从第一台计算机传输到第二台计算机,并在 PictureBox 中直接显示它:
void FirstComputer()
{
string fileToSend = @"C:\someImage.jpg";
string remoteAddress = "192.168.178.21";
int remotePort = 5432;
TcpConnectionHelper.SendFileConnecting(remoteAddress, remotePort, fileToSend);
}
void SecondComputer()
{
int localPort = 5432; // Must be the same as above
byte[] fileData = TcpConnectionHelper.ReceiveDataListening(localPort);
// The Bitmap handles the MemoryStream so no need to dispose it:
Bitmap image = new Bitmap(new MemoryStream(fileData));
pictureBox1.Image = image;
}
---
TelnetConnection
此类表示与 telnet 服务器的连接。您可以同时使用多个连接。您必须在构造函数中设置服务器地址、端口、用户名和密码。不带这些参数的构造函数使用标准值:用户名留空,端口设置为标准 telnet 端口 23。
调用 Connect()
方法时会建立连接。这会启动一个线程连接到远程服务器。如果收到“password:”或“login:”提示,它会输入构造函数中存储的密码;如果收到“username:”提示(不适用于 Fritz!Box telnet 登录),它会输入构造函数中存储的用户名。Connect()
方法也由公共方法在内部调用,以检查连接是否已建立或是否需要连接。连接处理在内部使用状态机架构。线程定期查找要接收的数据,然后查找要发送的数据。该类有三个私有 Action,用于处理接收到的数据:
Action<string> CurrentLineHandler
:必须将变量_conn_PushAllData
设置为 false 才能调用此 Action。每当连接线程遇到换行符时,都会使用捕获的行调用此 Action。Action<IEnumerable<byte>> CurrentRawDataHandler
:必须将变量_conn_PushAllData
设置为 true 才能调用此 Action。每当接收到任何数据时,都会使用接收到的数据调用此 Action。Action CurrentTimeoutHandler
:如果_conn_PushAllData
为 false,则当缓冲区中有数据但没有换行符且在超时时间内未接收到数据时,将调用此方法;如果为 true,则在超时时间内未接收到数据时,会定期调用此方法。
您可以设置 dummy 变量并通过调用 ResetConnHandlers()
方法将 _conn_PushAllData
重置为 false 。在设置 dummy 期间接收到的所有内容都将被忽略。
要发送数据,应调用 AddSendData(...)
方法,传递字节数组或字符串。字符串将使用 ASCII 编码转换为字节数组。数据以线程安全的方式存储在缓冲区中,并通过连接线程发送到服务器。
SendCommand(string cmd, int timeout)
方法将传入的 cmd 数据排队发送,然后等待直到在传入的超时时间(以毫秒为单位)内没有接收到新行,然后返回一个包含所有接收到的行的字符串。终端通常在发送结果之前将 cmd 数据回显给客户端——这些数据会被过滤掉。
GetRecursiveFileList(string rootPath, Action<string> onNewLine)
方法向服务器发送 LS 命令,然后每次收到新行时调用 onNewLine Action。TelnetDirectoryInfo
类中的 LoadCompleteTree(...)
方法使用此方法并解析行。
下载(DownloadFile*
)和上传(UplaodFile*
)文件各有三种方法。路径定义始终指完整路径,而不是相对路径。
- 以
*Active
结尾表示客户端在向 telnet 服务器发送 NC 命令后,使用第二个连接(使用 TcpConnectionHelper.[Send|Receive]DataConnecting 方法)主动连接到服务器。所需参数是 localPath、remotePath 和 remotePort。请注意,根据上传或下载,参数的顺序不同。 - 以
*Passive
结尾表示客户端在向 telnet 服务器发送 NC 命令后,监听由 telnet 服务器发起的第二个连接(使用 TcpConnectionHelper.[Send|Receive]DataListening 方法)。所需参数是 localPath、remotePath、localAddr 和 localPort。请注意,根据上传或下载,参数的顺序不同。如果 localAddr 参数为空,则方法将查询所有本地 IP 并选择第一个用于 NC 命令。 DownloadFileCat(...)
方法调用 telnet 服务器上的 CAT 命令,并使用现有连接将输出存储到文件中。所需参数是 remotePath 和 localPath。UplaodFileEcho(...)
方法将文件拆分成大约 1000 字节大小的块,并将它们包装在 echo 命令中,在 telnet 服务器上执行,只使用现有连接,并通过将命令的输出重定向到文件中来写入文件。所需参数是 localPath 和 remotePath。
Delete(...)
方法使用 RM 命令删除文件或目录树。所需参数是 remotePath 和 isDirectory。
CreateDirectory(...)
方法创建目录——如果需要,也包括父目录。必需参数是 remotePath。
这是一个示例,它使用 echo 命令将文件上传到远程服务器,然后使用服务器端监听的 netcat 下载(并删除)它。然后检查两个文件是否相同:
public void CheckUploadIdentical()
{
string baseFile = @"C:\myImage.jpg";
string remotePath = "/tmp/img_temp"
string remoteFile = remotePath + "/myImage.jpg";
string checkFile = @"C:\myImage_check.jpg";
// Establish a connection
TelnetConnection conn = new TelnetConnection("fritz.box", "MyPassword123");
conn.Connect();
// Upload the file making sure the folder exists
conn.CreateDirectory(remotePath);
conn.UploadFileEcho(baseFile, remoteFile);
// Download and delete the file and directory
conn.DownloadFileConnecting(remoteFile, checkFile, 5432);
conn.Delete(remoteFile, false); // Delete the remote file
conn.Delete(remotePath, true); // Delete the remote file directory
// Compare the two files
bool result = true;
using (FileStream s1 = new FileStream(baseFile, FileMode.Open, FileAccess.Read))
using (FileStream s2 = new FileStream(checkFile, FileMode.Open, FileAccess.Read))
{
while (true)
{
int b1 = s1.ReadByte();
int b2 = s2.ReadByte();
if (b1 == -1 && b2 == -1) // Both files ended without a difference
break;
if (b1 != b2) // A difference occured or one file ended before the other
{
result = false;
break;
}
}
}
if (result)
MessageBox.Show("The files are identical.");
else
MessageBox.Show("The files are different.");
}
---
TelnetDirectoryInfo
此类用于反射文件系统。您可以调用静态 LoadCompleteTree(...)
方法来加载由传递的 TelnetConnection conn 表示的 telnet 服务器上的目录树。传递的参数 rootPath 定义了哪个目录被用作起始目录——标准是 /。结果将始终是服务器的根目录,但只有传递的 rootPath 将填充数据。第三个参数是一个 Action<string> onNewPath ,每次从服务器接收到的数据中开始新的目录描述时都会调用它。它旨在(并且应用程序也这样使用)在用户需要等待时向用户提供反馈,表明工作仍在进行中。以下示例加载了完整的目录树,并计算了找到多少文件(不计算链接):
// This should be startet in its own thread
public TelnetDirectoryInfo LoadFiles()
{
// Establish a connection
TelnetConnection conn = new TelnetConnection("fritz.box", "MyPassword123");
conn.Connect();
// Load complete directory tree while showing the current path in a label
TelnetDirectoryInfo root = TelnetDirectoryInfo.LoadCompleteTree(conn, "/", path =>
{
this.Invoke(new Action(() => label1.Text = path));
});
// Show file count in the label
this.Invoke(new Action(() => label1.Text = CountFiles(root).ToString()));
return root;
}
private int CountFiles(TelnetDirectoryInfo currRoot)
{
int count = 0;
// Add all files that are not a link from this directory
count += currRoot.Files.Count(f => !f.IsLink);
// Add all files from sub-directories recursively
currRoot.SubDirectories.ForEach(s => count += CountFiles(s));
return count;
}
第二个示例递归地从 TelnetDirectoryInfo 中提取具有给定后缀的所有文件的路径——再次忽略链接:
public List<string> GetAllFilesEndingWith(TelnetDirectoryInfo root, string ending)
{
List<string> result;
// Find all files having the ending and extract the full path as string
result = root.Files.FindAll(f => !f.IsLink && f.Name.EndsWith(ending))
.Select(f => f.Path + "/" + f.Name).ToList();
// Add all files having the ending from all sub-directories
root.SubDirectories.ForEach(s => result.AddRange(GetAllFilesEndingWith(s, ending)));
return result;
}
历史
- 2014-09-10:在 GUI 中包含用户名,更新了源代码和二进制文件。