SSH terminal

Introduction

SSH allows remote connection to Unix, Linux or MacOS based servers directly from the terminal window. In this tutorial, you’ll learn how to build a very simple SSH bot that will allow you to connect to your SSH server on the Telegram messaging platform. i.e You could be anywhere in the world on vacation and still perform operations/commands (e.g restarting your server, checking error logs etc) on your SSH server through a Telegram chatbot as long as you have a smartphone with an active internet connection and the Telegram app. 

Key terms:

Telegram: is a cloud-based instant messaging and voice over IP service.

SSH Server: Secure Shell is a cryptographic network protocol for connecting and exchanging data remotely to/from a Unix, Linux or MacOS based server.

Redis: is an open source in-memory data structure store, used as a database, cache and message broker.

Predis: is a PHP client library for the Redis key-value store.

The code for this project can be found at => https://github.com/learningdollars/akinsanya-SSH-Telegram-bot


Technical Requirements (prerequisites)

Let’s run down through the Prerequisites and tools required to build our system:

  • Codeigniter (PHP)
  • Git
  • Composer
  • Ngrok Installed 
    • ( $ snap install ngrok )
  • SSH server
  • Redis Server
  • Predis
  • Phpseclib
  • Telegram Account
  • Linux OS (feel free to use any OS)

Part 1 – Telegram Chatbot Setup:

A Telegram account is required to create a Telegram chatbot. If you don’t already have a Telegram account, download the Telegram client and create a new account.

  • Launch BotFather in the telegram app and follow the steps to create a new chatbot.
  • Store the chatbot’s access token in your OS environment variable as it would be used to make HTTP requests and set up the webhook to your application. If you are running a Linux OS, open up your terminal and run:

$ export TELEGRAM_ACCESS_TOKEN=YOUR-TELEGRAM-ACCESS-TOKEN

Note: Replace YOUR-TELEGRAM-ACCESS-TOKEN with the token from your Telegram bot.

Read more about Telegram bots here: https://blog.learningdollars.com/2019/10/08/how-to-build-a-chatbot-with-telegram-wit-ai-dialogflow-and-codeigniter/ 

Part 2 – CodeIgniter Setup:

It’s assumed you already know the basics of CodeIgniter. Create a new CodeIgniter project by running;

$ git clone https://github.com/bcit-ci/CodeIgniter.git

Navigate to your project directory in your terminal and create a local web server by running;

$ php -S localhost:5000

Our local server should be available at http://localhost:5000

Part 3 – Ngrok Setup:

Open up a new terminal, navigate to your project directory and generate a Ngrok hosted public address for your web application by running;

$ ngrok http 5000

This public address or hostname would be used to configure the webhook URL for your Telegram Chatbot.

Part 4 – Redis Server Setup:

If you don’t already have Redis server installed on your computer, open up your terminal and install by running;

$ sudo apt-get install redis-server

To enable Redis to start on system boot, run;

$ sudo systemctl enable redis-server.service

The default redis host is 127.0.0.1 and port is 6379. Run these commands in your terminal to add them to your environment variables.

$ export REDIS_HOST=127.0.0.1

$ export REDIS_PORT=6379

If you are running on a different redis host and port, replace the REDIS_HOST and REDIS_PORT with the appropriate values.

Install Redis PHP extension by running;

$ sudo apt-get install php-redis

Part 5 – Bot Logic:

Navigate to application/config/config.php in your CodeIgniter project directory and update $config[‘base_url’] to your Ngrok public address. Also set $config[‘composer_autoload’] to TRUE.

Open up a new terminal, Navigate to application/ directory in your project and run the following;

$ composer require phpseclib/phpseclib:~2.0

$ composer require predis/predis

Create a new controller, `Flow.php` under application/controllers and a new model, `Telegram_model.php` under application/model.

‘Flow.php’

<?php
Predis\Autoloader::register();
use phpseclib\Net\SSH2;
 
defined('BASEPATH') or exit('No direct script access allowed');
 
class Flow extends CI_Controller
{
   public function __construct()
   {
       parent::__construct();
       $this->load->model('Telegram_model');
   }
 
   /**
    * Telegram webhook method.
    */
   public function webhook()
   {
       $input = json_decode(file_get_contents('php://input'), true);
       $userData = array();
       $userData['chat_id'] = $input['message']['chat']['id'] ?? $input['callback_query']['message']['chat']['id'];
       $userData['message'] = $input['message']['text'] ?? $input['callback_query']['data'];
       $userData['message_id'] = $input['message']['message_id'] ?? $input['callback_query']['message']['message_id'];
       $userData['first_name'] = $input['message']['from']['first_name'] ?? $input['callback_query']['from']['first_name'];
       $userData['last_name'] = $input['message']['from']['last_name'] ?? $input['callback_query']['from']['last_name'];
       $userData['username'] = $input['message']['from']['username'] ?? $input['callback_query']['from']['username'];
       $userData['payload'] = $input['callback_query']['data'] ?? null;
       $userData['callback_id'] = $input['callback_query']['id'] ?? null;
       $userData['connection'] = $this->redis('get', 'ssh-'+$userData['chat_id']) ?? null;
       $userData['command'] = $userData['message'] ?? null;
       $this->start($userData);
   }

/**
    * Handles the process flow
    */
   public function start($userData)
   {
       $menu = array(
           array( 'title'=>'ssh', 'payload'=>'ssh'),
           array( 'title'=>'cmd', 'payload'=>'cmd'),
           array( 'title'=>'reconnect', 'payload'=>'reconnect'),
           array( 'title'=>'disconnect', 'payload'=>'disconnect'),
           array( 'title'=>'help', 'payload'=>'help'),
       );
       if (strpos($userData['message'], '/start') !== false || strpos($userData['message'], 'help') !== false) {
           // if a user sends '/start', the default message is sent as a response
           $userData['bot_response'] = "*Hi there*, `".$userData['first_name']."`. Welcome to SSH!\n\nThis Simple BOT will allow you to establish an SSH connection. We currently support only password connection.\n\nAvailable Commands:";
           $userData['bot_data'] = $menu;
           $this->Telegram_model->sendInlineKeyboard($userData);
       } elseif (strpos($userData['message'], 'ssh') !== false) {
           $userData['connection'] = $userData['message'];
           if (strtolower($userData['message']) == 'ssh') {
               $this->ssh_default_response($userData);
           } else {
               $ssh = $this->login($userData);
               if (isset($ssh) && $ssh !== null) {
                   $host = $this->split_data($userData['connection'], 'host')[1];
                   $userData['bot_response'] = "You are now connected to $host";
               } else {
                   $userData['bot_response'] = "Login Failed";
               }
               $userData['bot_data'] = null;
               $this->Telegram_model->sendMessage($userData);
           }
       } elseif (strpos($userData['message'], 'cmd') !== false) {
           $userData['connection'] = $this->redis('get', 'ssh-'+$userData['chat_id']);
           $userData['command'] = $userData['message'];
           if (strtolower($userData['message']) == 'cmd') {
               return $this->cmd_default_response($userData);
           } else {
               return $this->cmd($userData);
           }
       } elseif (strpos($userData['message'], 'reconnect') !== false) {
           $userData['connection'] = $this->redis('get', 'ssh-'+$userData['chat_id']);
           $ssh = $this->login($userData);
           if (isset($ssh) && $ssh !== null) {
               $host = $this->split_data($userData['connection'], 'host')[1];
               $userData['bot_response'] = "You are now connected to $host";
           } else {
               $userData['bot_response'] = "Connection lost".$userData['connection'];
           }
           $userData['bot_data'] = null;
           $this->Telegram_model->sendMessage($userData);
       } elseif (strpos($userData['message'], 'disconnect') !== false) {
           $this->redis('set', 'ssh-'+$userData['chat_id'], '');
           $userData['bot_response'] = "You are now disconnected. Hope to see you again.";
           $userData['bot_data'] = null;
           $this->Telegram_model->sendMessage($userData);
       } else {
           $userData['bot_response'] = "Please type 'help' to view the available commands";
           $userData['bot_data'] = $menu;
           $this->Telegram_model->sendInlineKeyboard($userData);
       }
   }
/**
    * SSH default message
    */
   public function ssh_default_response($userData)
   {
       $userData['bot_response'] = "Please send your message in this format to authenticate ssh: `ssh --host=<VALUE> --user=<VALUE> --password=<VALUE>`";
       $userData['bot_data'] = null;
       $this->Telegram_model->sendMessage($userData);
   }
 
   /**
    * CMD default message
    */
   public function cmd_default_response($userData)
   {
       $userData['bot_response'] = "cmd <COMMAND>. Example: cmd pwd";
       $userData['bot_data'] = null;
       $this->Telegram_model->sendMessage($userData);
   }

Include Predis\Autoloader::register() and use phpseclib\Net\SSH2 to load predis and phpseclib packages in the code.

The webhook() method is the entry point to the application. It receives and handles incoming requests from telegram.

The start($userData) method accepts the user’s data array as a parameter. It handles the process flow and provides responses to the user’s message input.

/**
    * Splits the SSH connection string
    */
   public function split_data($data, $type)
   {
       $ssh = explode(" ", $data);
       if ($type == strtolower($ssh[0])) {
           return true;
       } elseif ($type == 'host') {
           return explode("=", $ssh[1]);
       } elseif ($type == 'user') {
           return explode("=", $ssh[2]);
       } elseif ($type == 'password') {
           return explode("=", $ssh[3]);
       } else {
           return null;
       }
   }
 
   /**
    * Authenticates the ssh username, password and hostname
    */
   public function login($userData)
   {
       $isSsh = $this->split_data($userData['connection'], 'ssh');
       if ($isSsh == true) {
           $host = $this->split_data($userData['connection'], 'host');
           $user = $this->split_data($userData['connection'], 'user');
           $password = $this->split_data($userData['connection'], 'password');
           $host_value = ($host[0] == '--host') ? $host[1] : null;
           $user_value = ($user[0] == '--user') ? $user[1] : null;
           $password_value = ($password[0] == '--password') ? $password[1] : null;
           if ($host_value == null || $user_value == null || $password_value == null) {
               if ($host_value == null) {
                   $userData['bot_response'] = 'Invalid Host';
               } elseif ($user_value == null) {
                   $userData['bot_response'] = 'Invalid User';
               } else {
                   $userData['bot_response'] = 'Invalid Password';
               }
               $userData['bot_data'] = null;
               $this->Telegram_model->sendMessage($userData);
               return null;
           }
           $ssh = new SSH2($host_value);
           if (!$ssh->login($user_value, $password_value)) {
               $userData['bot_response'] = 'Login Failed';
               $userData['bot_data'] = null;
               $this->Telegram_model->sendMessage($userData);
               return null;
           } else {
               $this->redis('set', 'ssh-'+$userData['chat_id'], $userData['connection']);
               return $ssh;
           }
       } else {
           return $this->ssh_default_response($userData);
       }
   }
  
   /**
    * Executes commands in the ssh server
    */
   public function cmd($userData)
   {
       $ssh = $this->login($userData);
       if (isset($ssh) && $ssh !== null) {
           $cmd = explode(" ", $userData['command']);
           if ($cmd[0] == 'cmd') {
               $exec = str_replace('cmd ', '', $userData['command']);
               $userData['bot_response'] = substr($ssh->exec($exec), 0, 4096);
               $userData['bot_data'] = null;
               return $this->Telegram_model->sendMessage($userData);
           } else {
               $userData['bot_response'] = 'Could not execute command';
               $userData['bot_data'] = null;
               $this->Telegram_model->sendMessage($userData);
               return $this->cmd_default_response($userData);
           }
       } else {
           $userData['bot_response'] = 'Authentication Failed';
           $userData['bot_data'] = null;
           return $this->Telegram_model->sendMessage($userData);
       }
   }
/**
    * Handles storing and fetching data from the redis store
    */
   public function redis($op="", $key="", $value="")
   {
       $HOST = getenv('REDIS_HOST');
       $PORT = getenv('REDIS_PORT');
       $client = new Predis\Client("tcp://$HOST:$PORT");
       if ($op == 'set') {
           $client->set($key, $value);
           $client->persist($key);
       } elseif ($op == 'get') {
           return $client->get($key);
       }
   }
}

The login($userData) method is authenticate the SSH credentials and the cmd($userData) method is used to perform operations on the server..

‘Telegram_model’  handles making HTTP requests to Telegram.

‘Telegram_model.php’

<?php
 
class Telegram_model extends CI_Model
{
   /**
    * Creates an array containing the required body for telegram HTTP request
    */
   public function sendMessage($userData)
   {
       $this->senderAction($userData, "typing");
       $data = [
                   'chat_id'=>$userData['chat_id'],
                   'text'=> $userData['bot_response'],
                   'parse_mode'=>'HTML',
                   'reply_to_message_id'=>null,
                   'reply_markup'=>null
               ];
       return $this->telegram(array('type'=>'sendMessage', 'data'=>$data));
   }
 
   public function sendInlineKeyboard($userData){
       $this->senderAction($userData, "typing");
       $data_arr = array();
       $data_desc = array();
       foreach($userData['bot_data'] as $res_data){
           $data_desc[] = $res_data['title'];
           $data_arr[] = array("inline_message_id"=>null,"text"=>$res_data['title'],"callback_data"=>$res_data['payload']);
       }
       if(count($data_arr) > 3){
           if(max(array_map('strlen', $data_desc)) >= 15){
               $keyboard_data = array_chunk($data_arr, 1);
           }  
           else{
               $keyboard_data = array_chunk($data_arr, 3);
           }      
       }
       else{
           $keyboard_data = [$data_arr];
       }
      
       $keyboard= array("inline_keyboard"=>$keyboard_data);
      
       $data = [
           'chat_id'=>$userData['chat_id'],
           'text'=>$userData['bot_response'],
           'parse_mode'=> 'HTML',
           'reply_to_message_id'=>null,
           'reply_markup'=>json_encode($keyboard)
           ];
      
       return $this->telegram(array('type'=>'sendMessage', 'data'=>$data));
   }
   /**
    * Sends the 'bot is typing' message
    */
   public function senderAction($userData, $sender_action)
   {
       $data = [
               'chat_id'=>$userData['chat_id'],
               'action'=>$sender_action
               ];
       return $this->telegram(array('type'=>'sendChatAction','data'=>$data));
   }
 
    /**
    * Sends response back to the user
    */
   public function telegram($data)
   {
       $token = getenv('TELEGRAM_ACCESS_TOKEN');
       $headers = array();
       $body = $data['data'];
       $url = "https://api.telegram.org/bot$token/".$data['type']."?".http_build_query($body);
       return $this->doCurl($url, $headers, '', '');
   }
 
    /**
    * Makes HTTP request
    */
   public function doCurl($url, $headers)
   {
       $ch = curl_init();
       curl_setopt($ch, CURLOPT_URL, $url);
       curl_setopt($ch, CURLOPT_POST, false);
       curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
       curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
       curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
      
       $res = curl_exec($ch);
       if (curl_error($ch)) {
           print_r(curl_error($ch));
       }
       curl_close($ch);
       return json_decode($res, true);
   }
}

You’re almost done building your simple SSH Telegram chatbot.

Part 6- Setting Telegram Webhook

A Webhook URL needs to be configured for Telegram to send HTTP requests and allow your application respond accordingly when a message is sent.

To set the webhook URL, run this in your terminal. 

$ curl https://api.telegram.org/bot$TELEGRAM_ACCESS_TOKEN/setWebhook?url=https://YOUR_NGROK_URL/index.php/flow/webhook

Replace YOUR_NGROK_URL with the URL generated when you started your Ngrok Server.

It’s assumed you already have an ssh server . In case you don’t have one, we can work with the server below:

Hostname: test.rebex.net
User: demo
Password: password

Sample conversation with the SSH Telegram chatbot

SSH Telegram bot conversation

Learning tools

More resources on building an SSH Telegram chatbots with codeigniter, predis and SSH,  can be found on the following sites:

https://core.telegram.org/bots/api
https://stackoverflow.com/search?q=telegram-bot-php
https://stackoverflow.com/questions/tagged/ssh
https://github.com/nrk/predis
https://github.com/phpseclib/phpseclib

Learning Strategy

During the course of this project, I was having difficulties with PHP sessions. I later discovered through an answer on stackoverflow that application state information is never saved on the server but saved by the client as session ID which makes PHP sessions stateless. So, I decided to use Redis key value store to save the session of a user.

Reflective Analysis

The traditional way of gaining access to a VPS is by opening up a terminal window and inputting the SSH connection string (user, host). Now, you can access your server anywhere you are in the world through a Telegram bot.

Conclusion & Next Steps

Now that you built the simple SSH bot, you can add new functionalities such as logging in to SSH server with RSA key. Project set up summary:

1. Clone the project from GitHub
2. Setup Redis/Predis
3. Generate a Ngrok public address that routes to your localhost web server port.
4. Create a telegram bot via botfather and set up your webhook URL.
5. Converse with your bot and connect to your server via SSH.

Project source code => https://github.com/learningdollars/akinsanya-SSH-Telegram-bot

Citation

Time Report

Estimated project Setup time: 10 hours.