Rebex

Skip to content, Skip to navigation




SSH Shell Tutorial

Back to tutorial list...

Table of content


# About Rebex SSH Shell

Rebex SSH Shell for .NET is a versatile SSH component for .NET languages (such as C# or VB.NET). It makes it possible to access remote servers from your application using SSH (Secure Shell) protocol. SSH is nearly ubiquitous on Unix and Unix-like systems, with OpenSSH being the most common implementation. There is also a growing number of SSH servers for Windows as well.

Most SSH servers also implement SFTP - a powerful secure file transfer protocol. To take advantage of it, use our SFTP component. An SFTP + SSH bundle is also available.

 

back to top...


# Namespaces and assemblies

To use the features of Rebex SSH Shell for .NET described here, you have to reference the Rebex.Net.SshShell.dll, Rebex.Net.Ssh.dll and Rebex.TerminalEmulation.dll assemblies in your project. These contain Ssh, Shell, TerminalControl/SshTerminalControl, VirtualTerminal and other classes in Rebex.Net and Rebex.TerminalEmulation namespaces.

In your source files, import the following namespace:

C#

using Rebex.Net;
using Rebex.TerminalEmulation;

VB.NET

Imports Rebex.Net
Imports Rebex.TerminalEmulation;

 

back to top...


# SSH basics - connecting, logging in and disconnecting

Typical SSH session goes like this:

  • Connect to the SSH server
  • Verify the server's fingerprint
  • Login - authenticate with user name and password
  • Execute commands and read responses
  • Disconnect

And now let's look at some sample code.

C#

// create client and connect 
Ssh client = new Ssh();

// to verify the server's fingerprint: 
//   a) check client.Fingerprint property after the 
//      'Connect' method call 
//    - or - 
//   b) use client.FingerprintCheck event handler 
//      to implement a fingerprint checker 

client.Connect(hostname);

// verify the server's fingerprint here unless using the event handler 

// authenticate  
client.Login(username, password);

// execute commands, start shells, etc... 
// ...  

// disconnect  
client.Disconnect();

VB.NET

' create client and connect 
Dim client As New Ssh

' to verify the server's fingerprint: 
'   a) check client.Fingerprint property after the 
'      'Connect' method call 
'    - or - 
'   b) use client.FingerprintCheck event handler 
'      to implement a fingerprint checker 
client.Connect(hostname)
' verify the server's fingerprint here unless using the event handler 

' authenticate  
client.Login(username, password)

' execute commands, start shells, etc... 
' ...  

' disconnect  
client.Disconnect()

During a single SSH session, multiple commands may be executed and multiple shells can be activated. Actually, multiple commands and shells may be active at the same time!

You might have noticed that the fingerprint verification is missing here. This is because implementing this is your responsibility. The fingerprint ensures the identity of the SSH server and is the same every time you connect to it. In fact, it is a hash of the server's public key. The Ssh class makes it available as a Fingerprint property (string), which makes it easy to display or check it. If you don't check the fingerprint each time before(!) logging in, you are making yourself vulnerable to the man-in-the-middle attacks! In practice, the following approach is recommended:

1) When connecting to an unknown server (for the first time), let the user check and accept the fingerprint. Then save it along with the hostname of the server.

2) When connecting to a known server (whose hostname and fingerprint is already known), check whether the fingerprint presented by the server corresponds to the fingerprint saved before.

It is also possible to authenticate using your private key instead of password. Check out Public key authentication for more information.

 

back to top...


# Executing commands

Executing a single command at the server is simple - just use the RunCommand command of the Ssh class:

C#

// create client, connect and log in  
Ssh client = new Ssh();
client.Connect(hostname);
client.Login(username, password);

// run the 'uname' command to retrieve OS info 
string systemName = client.RunCommand("uname -a");
// display the output 
Console.WriteLine("OS info: {0}", systemName);

// run the 'df' command to retrieve disk usage info 
string diskFree = client.RunCommand("df");
// display the output 
Console.WriteLine("Disk usage info:");
Console.WriteLine(diskFree);

VB.NET

' create client, connect and log in  
Dim client As New Ssh
client.Connect(hostname)
client.Login(username, password)

' run the 'uname' command to retrieve OS info 
Dim systemName As String = client.RunCommand("uname -a")
' display the output 
Console.WriteLine("OS info: {0}", systemName)

' run the 'df' command to retrieve disk usage info 
Dim diskFree As String = client.RunCommand("df")
' display the output 
Console.WriteLine("Disk usage info:")
Console.WriteLine(diskFree)

Obviously, this only works for commands that don't require user input (check out the interactive commands tutorial for those that do). Another drawback is that you only get the response when the command is over, which is inconvenient for commands that produce lots of output while they run (use StartCommand method if you need to read the response while the command is running). However, in many cases, the RunCommand method is perfectly suitable.

 

back to top...


# Reading command response

If the Ssh's RunCommand method is not suitable for you because you would like to read the response before the command is finished, just use the StartCommand method instead. This gives you an instance of Shell that can be used to read the response in many different ways. You can use the ReadAll method to read the complete response:

C#

// create client, connect and log in 
// ... 

// start the 'df' command to retrieve disk usage info 
Shell shell = client.StartCommand("df");

// read all response, effectively waiting for the command to end 
string response = shell.ReadAll();

// display the output 
Console.WriteLine("Disk usage info:");
Console.WriteLine(response );

VB.NET

' create client, connect and log in 
' ... 

' start the 'df' command to retrieve disk usage info 
Dim shell As Shell = client.StartCommand("df")

' read all response, effectively waiting for the command to end 
Dim response As String = shell.ReadAll()

' display the output 
Console.WriteLine("Disk usage info:")
Console.WriteLine(response)

Alternatively, there is ReadLine method that makes it possible to read the response line-by-line:

C#

// create client, connect and log in 
// ... 

// start the 'df' command to retrieve disk usage info 
Shell shell = client.StartCommand("df");

// read all response line-by-line 
while (true)
{
    // read a single response line 
    string line = shell.ReadLine();

    // a value of null indicates end of response 
    if (line == null)
        break;

    // display the line 
    Console.WriteLine(line);
}

VB.NET

' create client, connect and log in 
' ... 

' start the 'df' command to retrieve disk usage info 
Dim shell As Shell = client.StartCommand("df")

' read all response line-by-line 
While True 
    ' read a single response line 
    Dim line As String = shell.ReadLine()

    ' a value of null indicates end of response 
    If line Is Nothing Then Exit While 

    ' display the line 
    Console.WriteLine(line)
End While 

And finally, char-by-char response retrieval is also available through the ReadChar method:

C#

// create client, connect and log in 
// ... 

// start the 'df' command to retrieve disk usage info 
Shell shell = client.StartCommand("df");

// read all response char-by-char 
while (true)
{
    // read a single response character 
    char c = shell.ReadChar();

    // a value of Shell.EndOfResponse indicates end of response 
    if (c == Shell.EndOfResponse)
        break;

    // display the character 
    Console.Write(c);
}

VB.NET

' create client, connect and log in 
' ... 

' start the 'df' command to retrieve disk usage info 
Dim shell As Shell = client.StartCommand("df")

' read all response char-by-char 
While True 
    ' read a single response character 
    Dim c As Char = shell.ReadChar()

    ' a value of Shell.EndOfResponse indicates end of response 
    If c = shell.EndOfResponse Then Exit While 

    ' display the character 
    Console.Write(c)
End While 

At this point, you might be wondering why the class used to read the response is called Shell. Well, that's because the same class is also used as a shell API, making it easy to write code that works both with stand-alone remotely executed commands and command executed from a shell.

 

back to top...


# Using shell

In addition to running separate commands using RunCommand and StartCommand methods, it is also possible to request a shell session that can be used to run multiple commands. However, detecting where the response of one command ends is the tricky part - Unix shells were obviously not designed to be used programmatically and providing a shell API is quite a challenge. For this reason, we offer two different shell modes:

  • Well-know shell mode
  • Prompt-based command response ending detection mode

Well-known shell mode is the preferred one - it is compatible with most bash-like, tcsh-like and DOS-like shells out-of-the-box.

C#

// create client, connect and log in 
// ... 

// start the shell in the 'well-known' mode 
Shell shell = client.StartShell(ShellMode.WellKnownShell);

// start the 'df' command 
shell.SendCommand("df");

// read all response, effectively waiting for the command to end 
string response = shell.ReadAll();

// display the output 
Console.WriteLine("Disk usage info:");
Console.WriteLine(response);

// start other shell command if needed 

VB.NET

' create client, connect and log in 
' ... 

' start the shell in the 'well-known' mode 
Dim shell As Shell = client.StartShell(ShellMode.WellKnownShell)

' start the 'df' command 
shell.SendCommand("df")

' read all response, effectively waiting for the command to end 
Dim response As String = shell.ReadAll()

' display the output 
Console.WriteLine("Disk usage info:")
Console.WriteLine(response)

' start other shell command if needed 

Prompt-based command response ending detection mode can be used with other shells as well, but its drawback is that a prompt used to detect command response ending has to be set manually to match the prompt at the remote server. For this reason, the well-known shell mode should be used whenever possible. For sample prompts strings of common shells, check out the Telnet tutorial.

C#

// create client, connect and log in 
// ... 

// start the shell in the 'prompt' mode 
Shell shell = client.StartShell(ShellMode.Prompt);

// set the Prompt property to indicate the server's prompt  
// (strongly depends on the server) 
shell.Prompt = string.Format("regex:{0}@{1}:.+[$#] $", hostname, username);

// start the 'df' command 
shell.SendCommand("df");

// read all response, effectively waiting for the command to end 
string response = shell.ReadAll();

// display the output 
Console.WriteLine("Disk usage info:");
Console.WriteLine(response);

// start other shell command if needed 

VB.NET

' create client, connect and log in 
' ... 

' start the shell in the 'prompt' mode 
Dim shell As Shell = client.StartShell(ShellMode.Prompt)

' set the Prompt property to indicate the server's prompt  
' (strongly depends on the server) 
shell.Prompt = String.Format("regex:{0}@{1}:.+[$#] $", hostname, username)

' start the 'df' command 
shell.SendCommand("df")

' read all response, effectively waiting for the command to end 
Dim response As String = shell.ReadAll()

' display the output 
Console.WriteLine("Disk usage info:")
Console.WriteLine(response)

' start other shell command if needed 

To read the command response, any of the three methods described in the reading command response tutorial can be used as well.

 

back to top...


# Interactive commands

Some shell commands require user input and this makes them hard to use programmatically. Whenever possible, a non-interactive mode or another command should be used. However, if this is not an option, you can still use interactive commands with a bit of additional work - all you need to know is the string that indicates the command is waiting for user input. For example, this is how to handle the interactive version of the rm command:

C#

// see above for how to get an instance of the Shell class 

// start the 'rm' command 
shell.SendCommand("rm -i /tmp/*");

// repeat while the command is running 
while (shell.IsRunning)
{
    // read response until end or until '? ' is encountered 
    string result = shell.ReadAll("? ");

    // if '? ' was encountered, send Y for yes 
    if (result.EndsWith("? "))
    {
        shell.SendCommand("y");
    }
}

VB.NET

' see above for how to get an instance of the Shell class 

' start the 'rm' command 
shell.SendCommand("rm -i /tmp/*")

' repeat while the command is running 
While shell.IsRunning
    ' read response until end or until '? ' is encountered 
    Dim result As String = shell.ReadAll("? ")

    ' if '? ' was encountered, send Y for yes 
    If result.EndsWith("? ") Then 
        shell.SendCommand("y")
    End If 
End While 

The loop simply detects all questions ending with "? " and answers 'y' to them.

 

back to top...


# Terminal emulation with Windows Forms control

The TerminalControl and SshTerminalControl class make it very simple to add terminal emulation capabilities to your applications. Even writing a full-fledged SSH client in only a few lines of code is trivial:

C#

// get the terminal control object that was added 
// to the Form using the Visual Studio designer 
SshTerminalControl terminal = this.Terminal;

// set SSH server address and credentials 
terminal.ServerName = serverName;
terminal.UserName = username;
terminal.UserPassword = password;

// connect to the server 
terminal.Connect();

// ... 

VB.NET

' get the terminal control object that was added 
' to the Form using the Visual Studio designer 
Dim terminal As SshTerminalControl = Me.Terminal

' set SSH server address and credentials 
terminal.ServerName = serverName
terminal.UserName = username
terminal.UserPassword = password

' connect to the server 
terminal.Connect()

' ... 

In this example code snippet, the SshTerminalControl class was used. This is an extension of the TerminalControl class that is independent of SSH protocol or component and might be used for other purposes as well, as demonstrated by the ANSI Player sample that uses it for a file-based ANSI communication replay. For basic usage, however, check the Simple Winform SSH Client and Winform SSH Client samples instead.

 

back to top...


# Virtual terminal emulation

In some scenarios where no user interaction is needed or desired and/or it is not necessary or desirable to display the terminal screen content in real time, a Windows Forms based terminal emulator might not be the right choice. The VirtualTerminal class may be more suitable. For example, there is no need to use WinFormClient if all you need is to save the content of the terminal screen into a file:

C#

// create client, connect and log in  
Ssh client = new Ssh();
client.Connect(serverName);
client.Login(username, password);

// create a virtual terminal 
VirtualTerminal terminal = new VirtualTerminal(80, 25);

// bind the virtual terminal to the SSH client object 
terminal.Bind(client);

// send the 'df' command to the server 
// please note that you also have to send the 'enter' - '\r' character 
terminal.SendToServer("df\r");

// receive and process any server response 
TerminalState state;
do 
{
    // wait 5 seconds for the response 
    state = terminal.Process(5000);

    // if there was a response, try processing more 
} while (state == TerminalState.DataReceived);

// save the content of the virtual terminal screen to a file 
terminal.Save("terminal.png", TerminalCaptureFormat.Png);

VB.NET

' create client, connect and log in  
Dim client As New Ssh()
client.Connect(serverName)
client.Login(username, password)

' create a virtual terminal 
Dim terminal As New VirtualTerminal(80, 25)

' bind the virtual terminal to the SSH client object 
terminal.Bind(client)

' send the 'df' command to the server 
' please note that you also have to send the 'enter' - the vbCr character 
terminal.SendToServer("df" & vbCr)

' receive and process any server response 
Dim state As TerminalState
Do 
    ' wait 5 seconds for the response 
    state = terminal.Process(5000)

    ' if there was a response, try processing more 
Loop While state = TerminalState.DataReceived

' save the content of the virtual terminal screen to a file 
terminal.Save("terminal.png", TerminalCaptureFormat.Png)

 

back to top...


# Terminal emulation options

The SshTerminalControl/TerminalControl and VirtualTerminal classes have lots of options to make them compatible with lots of different terminals. For more information about these, check out the documentation for the TerminalOptions class or the Winform SSH Client sample.

C#

// create a virtual terminal 
VirtualTerminal terminal = new VirtualTerminal(80, 25);

// initialize the options class and set some options 
TerminalOptions options = new TerminalOptions();
options.TerminalName = "xterm";
options.FunctionKeysMode = FunctionKeysMode.Linux;
options.Encoding = System.Text.Encoding.GetEncoding("iso-8859-1");

// assign the options 
terminal.Options = options;

VB.NET

' create a virtual terminal 
Dim terminal As New VirtualTerminal(80, 25)

' initialize the options class and set some options 
Dim options As New TerminalOptions()
options.TerminalName = "xterm"
options.FunctionKeysMode = FunctionKeysMode.Linux
options.Encoding = System.Text.Encoding.GetEncoding("iso-8859-1")

' assign the options 
terminal.Options = options

 

back to top...


# Recording communication

The TerminalControl and VirtualTerminal classes have a useful and unique feature - communication recording. This makes it possible to save a complete SSH shell session into a file and replay it later using our ANSI Player application.

C#

// get the terminal control object 
TerminalControl terminal = this.Terminal;

// set the recorder to a file-based stream writer 
terminal.Recorder = System.IO.File.AppendText("recording.ans");

// ... 

VB.NET

' get the terminal control object 
Dim terminal As TerminalControl = Me.Terminal

' set the recorder to a file-based stream writer 
terminal.Recorder = System.IO.File.AppendText("recording.ans")

' ... 

 

back to top...


# Getting information about SSH connection

You can easily get information about the SSH connection using properties of Ssh.Session that makes the underlying Rebex.Net.SshSession class available. Please note that you also need to reference Rebex.Net.Ssh.dll assembly from your project to be able to use the SshSession class.

C#

Ssh client = new Ssh();
client.Connect(hostname);

// The server's fingerprint 
Console.WriteLine("Fingerprint: {0}", client.Fingerprint);

// The Cipher property contains a lot of 
// information about the current cipher 
SshCipher cipher = client.Session.Cipher;
Console.WriteLine("Host key algorithm: {0}", cipher.HostKeyAlgorithm);
Console.WriteLine("Key exchange algorithm: {0}", cipher.KeyExchangeAlgorithm);
Console.WriteLine("Incoming MAC algorithm: {0}", cipher.IncomingMacAlgorithm);
Console.WriteLine("Incoming cipher: {0}", cipher.IncomingEncryptionAlgorithm);
Console.WriteLine("Outgoing MAC algorithm: {0}", cipher.OutgoingMacAlgorithm);
Console.WriteLine("Outgoing cipher: {0}", cipher.OutgoingEncryptionAlgorithm);
Console.WriteLine("Summary: {0}", cipher);

VB.NET

Dim client As New Ssh
client.Connect(hostname)

' The server's fingerprint 
Console.WriteLine("Fingerprint: {0}", client.Session.Fingerprint)

' The Cipher property contains a lot of 
' information about the current cipher 
Dim cipher As SshCipher = client.Session.Cipher
Console.WriteLine("Host key algorithm: {0}", cipher.HostKeyAlgorithm)
Console.WriteLine("Key exchange algorithm: {0}", cipher.KeyExchangeAlgorithm)
Console.WriteLine("Incoming MAC algorithm: {0}", cipher.IncomingMacAlgorithm)
Console.WriteLine("Incoming cipher: {0}", cipher.IncomingEncryptionAlgorithm)
Console.WriteLine("Outgoing MAC algorithm: {0}", cipher.OutgoingMacAlgorithm)
Console.WriteLine("Outgoing cipher: {0}", cipher.OutgoingEncryptionAlgorithm)
Console.WriteLine("Summary: {0}", cipher)

 

back to top...


# Specifying SSH parameters

It is possible to affect many aspects of SSH such us specifying key exchange algorithm to use or which cipher suites to allow.

Depending on your scenario, it might be a good idea to disable weak cipher suites and only allowing the strong ones.

Please note that you also need to reference Rebex.Net.Ssh.dll assembly from your project to be able to use the SshParameters class.

C#

// Create an instance of SshParameters class 
// to specify desired arguments. 
SshParameters par = new SshParameters();

// Any key exchange method is acceptable. 
par.KeyExchangeAlgorithms = SshKeyExchangeAlgorithm.Any;

// Only allow AES and 3DES encryption methods. 
par.EncryptionAlgorithms = SshEncryptionAlgorithm.AES | SshEncryptionAlgorithm.TripleDES;

// Connect to the server. 
// The third argument refers to the parameters class. 
client.Connect(hostname, Ssh.DefaultPort, par);

VB.NET

' Create an instance of SshParameters class 
' to specify desired arguments. 
Dim par As New SshParameters

' Any key exchange method is acceptable. 
par.KeyExchangeAlgorithms = SshKeyExchangeAlgorithm.Any

' Only allow AES and 3DES encryption methods. 
par.EncryptionAlgorithms = SshEncryptionAlgorithm.AES Or SshEncryptionAlgorithm.TripleDES

' Connect to the server. 
' The third argument refers to the parameters class. 
client.Connect(hostname, Ssh.DefaultPort, par)

 

back to top...


# Public key authentication

Instead of using a password to authenticate, it is often desirable to use public key cryptography. The SSH server keeps a database of public keys for each account - exact details of this depends on the server. For example, OpenSSH keeps the public keys in ~/.ssh/authorized_keys file in each account's home directory. Each public key has a corresponding private key that is kept secret by the client, usually in a file encrypted using a password. To authenticate to the server using public key cryptography, you need this private key. To generate the public/private key pair, either use the Key Generator sample application or another utility such as OpenSSH's ssh-keygen. Or write your own key generator using Rebex SSH Shell classes.

To authenticate using your private key, you have to load it into an SshPrivateKey instance. SshPrivateKey supports all the common private key file formats: PKCS#8 v1 and v2 or the traditional SSLeay compatible format. Support for other formats may be added on request.

Please note that you also need to reference Rebex.Net.Ssh.dll assembly from your project to be able to use the SshPrivateKey class.

C#

// create client and connect 
Ssh client = new Ssh();
client.Connect(hostname);

// verify the server's fingerprint (client.Fingerprint) 

SshPrivateKey privateKey = new SshPrivateKey("key_rsa.pem", "password");

// authenticate 
client.Login(username, privateKey);

// ... 

VB.NET

' create client and connect 
Dim client As New Ssh
client.Connect(hostname)

' verify the server's fingerprint (client.Fingerprint) 

Dim privateKey As New SshPrivateKey("key_rsa.pem", "password")

' authenticate 
client.Login(username, privateKey)

' ... 

Fingerprint verification was already discussed in SSH basics.

 

back to top...


# Private and public key generation

Rebex SSH Shell can generate the private and public keys to use within public key authentication. To generate a key, use SshPrivateKey class's Generate method. Once the RSA or DSA key is generated, save it using Save and SavePublicKey methods, or use GetPublicKey to get the raw data of the public key.

To be able to authenticate using your newly generated key, the public key has to be added to SSH server's database for your account. For example, in OpenSSH, the public keys are kept in ~/.ssh/authorized_keys each account's home directory.

The following code snippet shows how to generate a private key, save it (also the associated public key) and constructs the string to be added to OpenSSH ~/.ssh/authorized_keys file.

Please note that you also need to reference Rebex.Net.Ssh.dll assembly from your project to be able to use the SshPrivateKey class.

C#

// select the key algorithm (RSA or DSA) and key size 
SshHostKeyAlgorithm algorithm = SshHostKeyAlgorithm.RSA;
int keySize = 1024;

// generate private key 
SshPrivateKey privateKey = SshPrivateKey.Generate(algorithm, keySize);

// save the private key 
privateKey.Save("key_rsa.pem", "password", null);

// save the public key 
privateKey.SavePublicKey("key_rsa.pub");

// the only purpose of the rest of this sample 
// is to construct the OpenSSH-style public key string 

// get the raw form of SSH public key 
byte[] rawPublicKey = privateKey.GetPublicKey();

// select the appropriate algorithm id 
string publicKeyAlgorithm;
switch (algorithm)
{
    case SshHostKeyAlgorithm.RSA:
        publicKeyAlgorithm = "ssh-rsa";
        break;
    case SshHostKeyAlgorithm.DSS:
        publicKeyAlgorithm = "ssh-dss";
        break;
    default:
        throw new ApplicationException("Unsupported algorithm.");
}

// the string to be added to OpenSSH's ~/.ssh/authorized_keys file 
string sshPublicKey = string.Format("{0} {1} username@hostname",
    publicKeyAlgorithm,
    Convert.ToBase64String(rawPublicKey));

// and display it 
Console.WriteLine("Add the following line to your ~/.ssh/authorized_keys file:");
Console.WriteLine(sshPublicKey);
Console.WriteLine("(Modify 'username@hostname' to match your username and hostname.)");

VB.NET

' select the key algorithm (RSA or DSA) and key size 
Dim algorithm As SshHostKeyAlgorithm = SshHostKeyAlgorithm.RSA
Dim keySize As Integer = 1024

' generate private key 
Dim privateKey As SshPrivateKey = SshPrivateKey.Generate(algorithm, keySize)

' save the private key 
privateKey.Save("key_rsa.pem", "password", Nothing)

' save the public key 
privateKey.SavePublicKey("key_rsa.pub")

' the only purpose of the rest of this sample 
' is to construct the OpenSSH-style public key string 

' get the raw form of SSH public key 
Dim rawPublicKey As Byte() = privateKey.GetPublicKey()

' select the appropriate algorithm id 
Dim publicKeyAlgorithm As String 
Select Case algorithm
    Case SshHostKeyAlgorithm.RSA
        publicKeyAlgorithm = "ssh-rsa"
    Case SshHostKeyAlgorithm.DSS
        publicKeyAlgorithm = "ssh-dss"
    Case Else 
        Throw New ApplicationException("Unsupported algorithm.")
End Select 

' the string to be added to OpenSSH's ~/.ssh/authorized_keys file 
Dim sshPublicKey As String = String.Format("{0} {1} username@hostname", _
    publicKeyAlgorithm, _
    Convert.ToBase64String(rawPublicKey))

' and display it 
Console.WriteLine("Add the following line to your ~/.ssh/authorized_keys file:")
Console.WriteLine(sshPublicKey)
Console.WriteLine("(Modify 'username@hostname' to match your username and hostname.)")

 

back to top...


Back to tutorial list...