Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Telnet File Explorer

4.93/5 (8 votes)
2 Jun 2014CPOL16 min read 32.7K   2.1K  
A simple explorer and file transfer for files over a telnet connection

Introduction

WARNING: This code is designed to manipulate files on a remote system. Be sure you know what you do before you use it!

---

Out of pure curiosity I was playing a bit with my AVM Fritz!Box at home looking around, searching what you could do with it - besides letting it be a router of course. You can activate a Telnet server by calling #96*7* from a phone connected to the Fritz!Box (#96*8* to deactivate). So, I started looking around on the file system and found a lot LUA scripts which I wanted to have a closer look at. But simply using cat and having to scroll through the lines in a PuTTY console wasn't quite comfortable so I was looking for a way to copy those files to my computer because Notepad++ supports syntax highlighting for LUA scripts and it's much easier to handle different files simultaneously. Also navigation through the directory tree with cd and ls didn't really satisfy me as a Windows user. ;b

I started a Google search for a Telnet File Explorer but couldn't find one - ok, to be honest I didn't search really deeply because I like this kind of challenge. So, I began to write such an explorer which in this project is specialized to be used with a Fritz!Box but should work on any Telnet server with minor changes.

---

Background

The remote side: Basic Linux Commands

The AVM Fritz!Box uses a *nix like BusyBox system where many standard commands are available (See this German page for a complete list). I'm not used to use *nix like systems as much as I would like to, so let's have a look at the commands I'm using on the remote Telnet machine - if you're familiar with this, be welcome to skip it:

Command 'LS'

ls [-1AacCdeFilnpLRrSsTtuvwxXhk] [{filenames}] 

While in Windows you use dir in *nix systems you always use ls. There are a lot command line switches but I'll only explain those I will be using:

Argument Description
 
-a Show hidden entries (starting with a dot '.')
 
-l Show a detailed list with a one entry per line
 
-e Show a complete TimeStamp with weekday, seconds and year
 
-R Recursively list all sub-directories
 
{filenames} A start directory and/or a filter for the listing
 

To get all information I want, I use all of these arguments like this:

ls -a -l -e -R /   

This would result in a output like this:

./:
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 

Note that I only found these dots '.' before the path descriptions (those stopping with a colon ':') when I used the root path as a start directory - otherwise they started with a slash '/'. The information listed here for each file or folder is this:

Part Description
 
1<sup>st</sup> char of 1<sup>st</sup> block It's ether a 'd' for directory, 'l' for link or '-' for a normal file.
 
rest of 1<sup>st</sup> block Permissions: 3 times 'rwx' for 'r'=read, 'w'=write and 'x'=execute, if not allowed, there is a dash '-' at that position. The first 'rwx' is for root users, the second for the current group and the third for all users.
 
2<sup>nd</sup> block Number of sub-entries
 
3<sup>rd</sup> block Owner
 
4<sup>th</sup> block Group
 
5<sup>th</sup> block Size in bytes
 
6<sup>th</sup> to 10<sup>th</sup> block Modification date and time
 
11<sup>th</sup> block File or directory name
 
12<sup>th</sup> and 13<sup>th</sup> block If exists, the link target
 

On the client side I'll simply grab a snapshot of the whole tree at start-up. This takes some time but is much less prone to errors than dynamically loading single parts afterwards.

Command 'CAT'

cat {filename} 

Simply outputs the content of a file to the console. Although this method if prone to changing the output a bit, it is the only way to get a files content without an addition connection on a Fritz!Box because there is no hexdump command or an equivalent to convert non-printable chars. Also a line-break will always be converted to the current new-line standard which in my case is <CR><LF> even if the original file only uses <LF>. For text files this doesn't matter that much but it is impossible to load a binary file that way.

Command 'NC'

nc [{addr} {port}|-l -p {port}] [<|>] {filename} 

'NC' means netcat, so it's cat over the net. This is not the complete functionality on netcat but what I intend to use. It uses a separate connection to send or receive data while you can set files as input or output. Here's what the command line switches mean:

Argument Description
 
{addr} {port} Defines the target address and port to connect to
 
-l -p {port} Starts NC as a listener waiting for a connection on the port
 
< {filename} Defines a input file which content is to be sent as soon as a connection is established
 
> {filename} Defines a output file to write received data to as soon as a connection is established
 

In general there are two ways of exchanging files:

  • client->server: The server opens a port and the client connects to it
  • client<-server: The client opens a port and the server connects to it

It doesn't matter if there is a file to be sent or received - what matters is the environment of the two systems. Depending on which side can open a reachable port easier that one should be the host opening the port. In either way files can be transferred in both directions.

Here two examples for copying files from the server to the client (first the server, then the client listening):

nc -l -p 4322 < /home/afile.txt
nc 192.168.178.20 4321 < /home/afile.txt

and two analogical examples for copying files from the client to the server:

nc -l -p 4322 > /home/newfile.txt
nc 192.168.178.20 4321 > /home/newfile.txt 

Command 'ECHO'

This command is usually used to output text lines on the screen within scripts but if redirected it can be used to create text files. The syntax with redirection is:

echo -n $'{some text}' [>|>>] {filename} 

The parameter -n prevents the echo command to append a tailing newline.

The file data has to be escaped though in some cases. I'll be using strict quoting - the text will be enclosed like $'{sometext}' and I will escape most chars for compatibility:

  • A zero character (0x00) cannot be written because it terminates the string.
  • All chars with a value below 0x20 are control char and will be escaped with octal escaping format: 0xnnn where nnn is the three digit octal value of the char.
  • All chars with a value above 0x7F are extended ASCII chars and will also be escaped with octal escaping format as above.
  • All 'single quote' chars (0x27) obviously have to be escaped because they would otherwise end the quotation, so I'll use \' as direct escaping.
  • The backslash char (0x5C) will be escaped directly with \\.
  • All other chars will be transferred as they are.

This way, I do not have to send a file line by line but can split it in parts. Although most *nix machines do not have a quickly reached command-line length limitation of 8191 chars (resp. 2047 on older versions) like Windows, it seems a good idea to me though not to send everything in one command but to chunk it in about 1000 char segments. This is an example:

echo -n $'<html>\012<head><title>\\\'</title></head>\012<body></body>\012</html>' > n.txt 

It will create a file n.txt containing four lines:

<html>
<head><title>\'</title></head>
<body></body>
</html> 

This method should (like cat) only be used for text files of course because there might be some other chars besides 0x00 that cannot be written as intended.

Command 'MKDIR'

This command creates a new directory. If used with the command-line option -p all non-existing parent directories will be created too. Here is an example:

mkdir -p /var/tmp/newfolder/andsubfolder

Command 'RM'

This command can remove (delete) a file or directory tree. If used with the the command-line option -f there will be no confirmation prompt; if -r is used a directory and all its content is deleted recursively. Here are two examples - first deleting a file, then deleting a directory tree:

rm -f /usr/file.tmp
rm -f -r /var/tmp/my_tmp_folder

---

The client side

General code structure

I am a bit more comfortable with the client side because C#.NET is something I am familiar with. First I will explain the construct of my code. Here is how the classes are connected:

Classes UML

  • The class TelnetFileInfo is simple a data class which will store Information about a file.
  • The class TelnetDirectoryInfo extends the TelnetFileInfo class, adds a List of TelnetDirectoryInfo as SubDirectories and a List of TelnetFileInfo as Files and a method to load a complete file tree with a specified start directory. This class will store a directory/file tree. To load a complete tree you will have to pass a TelnetConnection instance - that is, why it's marked 'uses TelnetConnection'.
  • The class TelnetConnection is the heart of this project as it handles most of the logic concerning the telnet communication. For Sending and receiving files using netcat it uses the class TcpConnectionHelper to handle a connecting or listening second connection.
  • The static class TcpConnectionHelper is a helper to create connecting or listening connections and do automated data transfer as soon as connected.

General work-flow

Image 2 Image 3

I designed a simple Form with a TreeView on the left and a ListView an the right side. Below the ListView I added some controls to initiate uploads and downloads. In Addition I designed a simple Window to show status information because the download takes some time so it is running in a Thread and the user gets to see that something is done.

The TreeView will show the Directory tree after it is loaded. There are three different types of entries which have different icons and colors representing them:

  • Directories are displayed in red, enclosed in brackets and have a folder icon.
  • Files are displayed black with a common document icon.
  • Links are displayed blue with an empty sheets icon having a green arrow at it. Note that links can be targeted at files as well as at directories which is not differenced.

The ListView will simply display the information of the currently selected entry.

The 'Download Method' DropDownMenu (which I will rename to 'Transfer Method') sets one of the following three methods to upload or download files (or directories):

  • NETCAT client >> server: A second connection is used to connect to the server on the specified port using NETCAT on the server to receive or send a file.
  • NETCAT client << server: A second connection is used listening on the specified port until the server connects to it and sends or receives a file.
  • CAT/ECHO (text files only!!!): The existing connection is used to send or receive a file via console input or output. This is (like explained above) NOT suitable for binary files but in case the environment does neither allow the client nor the server to open another listening port this method can be used as a fall-back to at least get text files transferred.

Using the application you should do the following steps:

  • Enter your password in the Password TextBox.
  • Press [Enter] or click the connect button and wait until the directory tree is loaded.
  • To Download: Select a file or directory, enter a suitable port in the '* port:' TextBox and click the 'Download' button, select a target, click the 'OK' button and wait until the operation is complete.
  • To Upload: Select a directory, enter a suitable port in the '* port:' TextBox and click the 'Upload' button, select a file to upload, click the 'OK' button and wait until the operation is complete. For now it is only possible to upload files into existing directories.

Remember, that there is little or no error handling or input check so be careful with what you do!

---

Using the code

I will explain the use of the code from the bottom up.

TcpConnectionHelper

The methods ConnectToClient and WaitForConnection are internally used. I only made them public to have a possibility to use customized actions. There are description on how to use inside the code.

All SendData* and ReceiveData* methods are using a similar pattern as they are only different constellations and wrappers for each other. They are also well documented in the code. These methods are designed to work as an end-point for netcat started on the remote system. Because netcat does not automatically terminate the connection (unless you set a timeout which I did not try) I designed these methods to end the connection after a constant timeout without data transferred. All Methods ending with *Connecting use the ConnectToClient method internally and are connecting actively to the specified address and port from the arguments. All methods ending with *Listening use the WaitForConnection method internally and are waiting for the remote to connect to the specified local port from the arguments. In each case you can use either a byte array, a file path or a Stream to transfer data. All methods using byte array or file path as arguments (or return value) are simply wrappers for the corresponded method using a Stream. The following example shows how to transfer an image file from the first computer computer to a second which displays it directly in a PictureBox:

C#
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

This class represents a connection to a telnet server. You can use several connection simultaneously. You have to set the server address, port, username and password in the constructor. Constructors which do not have this parameters are using standard values: The username is left empty and the port is set to the standard telnet port 23.

A connection is started when the Connect() method is called. This starts a Thread which connects to the remote server. If a 'password:' or 'login:' prompt is received, is enteres the stored password from the constructor, if a 'username:' prompt is received (does not apply to Fritz!Box telnet login), it enters the stored username from the constructor. The Connect() method is also called internally by public methods to check if a connection is established or to connect if not. The connection handling uses a state machine architecture internally. The thread periodically looks for data to receive and then for data to be send. The class has three private Actions which are used to handle received data:

  • Action<string> CurrentLineHandler: The variable _conn_PushAllData must be false for this Action to be called. Whenever the connection thread encounters a line-break, this Action is called with the captured line.
  • Action<IEnumerable<byte>> CurrentRawDataHandler: The variable _conn_PushAllData must be true for this Action to be called. Whenever any data is received this Action is called with the data received.
  • Action CurrentTimeoutHandler: If _conn_PushAllData is false this is called when there is data in the buffer but no line-break and no data is received within the timeout, if true it gets called periodically when no data has been received within the timeout.

You can set dummies and reset _conn_PushAllData to false by calling the ResetConnHandlers() method. Everything received while the dummies are set is ignored.

To send data the AddSendData(...) method should be called either passing a byte array or a string. A string will be converted to a byte array using ASCII encoding. The data is store in a thread-safe way into a buffer and sent to the server by the connection thread.

The SendCommand(string cmd, int timeout) method queues the passed cmd data to be sent and then waits until no new line is received within the passed timeout in milliseconds to return a string containing all received lines. The terminal usually sends the cmd data back to the client before sending the result - this data is filtered out.

The GetRecursiveFileList(string rootPath, Action<string> onNewLine) method sends a LS command to the server and then calls the onNewLine Action every time a new line was received. The LoadCompleteTree(...) method in the TelnetDirectoryInfo class uses this method and parses the lines.

There are three methods each for downloading (DownloadFile*) and uploading (UplaodFile*) files. Path definitions are always meant to be full paths not relative ones.

  • The ending *Active is referring to the client connecting actively to the server using a second connection (the TcpConnectionHelper.[Send|Receive]DataConnecting methods are used) after sending a NC command to the telnet server. The required parameters are localPath, remotePath and remotePort. Note that depending an uploading or downloading the path parameters are in different order.
  • The ending *Passive is refering to the client listening for a second connection initiate by the telnet server (the TcpConnectionHelper.[Send|Receive]DataListening methods are used) after sending a NC command to the telnet server. The required parameters are localPath, remotePath, localAddr and localPort. Note that depending an uploading or downloading the path parameters are in different order. If the localAddr parameter is empty the methods queries all local IPs and picks the first to use for the NC command.
  • The method DownloadFileCat(...) calls a CAT command on the telnet server and stores the output into a file using only the existing connection. The required parameters are remotePath and localPath.
  • The method UplaodFileEcho(...) method splits up a file in about 1000 byte sized chunks and wraps them up in echo commands executed on the telnet server using only the existing connection and writing a file by redirecting the command's output into it. The required parameters are localPath and remotePath.

The method Delete(...) deletes a file or directory tree using a RM command. The required parameters are remotePath and isDirectory.

The method CreateDirectory(...) creates a directory - if necessary with parent folders. The required parameter is remotePath.

Here is an example which uploads a file to the remote server using the echo command then downloads (and deletes) it using netcat listening on the server side. It then checks if both files are identical:

C#
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

This class is used to reflect the file system. You can call the static LoadCompleteTree(...) method to load a directory tree from the telnet server represented by the passed TelnetConnection conn. The passed parameter rootPath defines which directory is used as a start directory - the standard is /. The result will be always be the root directory of the server but only the passed rootPath will be filled with data. The third parameter is an Action<string> onNewPath that is called every time a new directory description started in the data received from the server. It is intended (and in that way used by the application) to give a feedback to the user that there is still work going on when he has to wait. The following example loads the complete directory tree and counts how many files were found (not counting links):

C#
// 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;
}

This second example extracts all paths of files which have a given ending from a TelnetDirectoryInfo recursively - again ignoring links:

C#
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;
}

 

History

  • 2014-09-10: Included username in GUI, updated source and binary

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)