Advertisement
wzul

PHP SSH Tunneling

Nov 27th, 2024
104
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 7.72 KB | None | 0 0
  1. <?php
  2.  
  3. class SshTunnel
  4. {
  5.     public $localPort;
  6.     protected $sshCommand;
  7.     protected $verifyCommand;
  8.     protected $lsofCommand;
  9.     protected static $assignPort = 2049;
  10.  
  11.     protected $sshProcess;
  12.     public $sshUsername;
  13.     public $sshHost;
  14.     public $sshPort;
  15.     public $localAddress;
  16.     public $bindHost;
  17.     public $bindPort;
  18.     public $identityFile;
  19.     public $waitMs;
  20.     public $tries;
  21.     public $sshOptions;
  22.     public $autoConnect;
  23.     public $autoDisconnect;
  24.     public $sshPath;
  25.     public $lsofPath;
  26.     public $ncPath;
  27.  
  28.     public function __construct(
  29.         $sshUsername,
  30.         $sshHost,
  31.         $sshPort = 22,
  32.         $localAddress = '127.0.0.1',
  33.         $localPort = 0,
  34.         $bindHost = '127.0.0.1',
  35.         $bindPort = 3306,
  36.         $identityFile = '',
  37.         $waitMs = 1000000,
  38.         $tries = 10,
  39.         $sshOptions = ['-q'],
  40.         $autoConnect = true,
  41.         $autoDisconnect = true,
  42.         $sshPath = 'ssh',
  43.         $lsofPath = 'lsof',
  44.         $ncPath = 'nc'
  45.     ) {
  46.  
  47.         $this->sshUsername = $sshUsername;
  48.         $this->sshHost = $sshHost;
  49.         $this->sshPort = $sshPort;
  50.         $this->localAddress = $localAddress;
  51.         $this->localPort = $localPort;
  52.         $this->bindHost = $bindHost;
  53.         $this->bindPort = $bindPort;
  54.         $this->identityFile = $identityFile;
  55.         $this->waitMs = $waitMs;
  56.         $this->tries = $tries;
  57.         $this->sshOptions = $sshOptions;
  58.         $this->autoConnect = $autoConnect;
  59.         $this->autoDisconnect = $autoDisconnect;
  60.         $this->sshPath = $sshPath;
  61.         $this->lsofPath = $lsofPath;
  62.         $this->ncPath = $ncPath;
  63.    
  64.         if (!empty($lsofPath)) {
  65.             $this->lsofCommand = sprintf(
  66.                 '%s -P -n -i :%%d',
  67.                 escapeshellcmd($lsofPath)
  68.             );
  69.         }
  70.  
  71.         $this->localPort = $localPort;
  72.         if (empty($this->localPort)) {
  73.             do {
  74.                 $this->localPort = self::$assignPort++;
  75.             } while ($this->localPort <= 65535 and false === $this->isLocalPortAvailable());
  76.         }
  77.  
  78.         if (!empty($identityFile)) {
  79.             $sshOptions = array_merge(
  80.                 $sshOptions,
  81.                 [
  82.                     '-i',
  83.                     $identityFile
  84.                 ]
  85.             );
  86.         }
  87.         $this->sshCommand = array_merge(
  88.             [
  89.                 $sshPath
  90.             ],
  91.             $sshOptions,
  92.             [
  93.                 '-N',
  94.                 '-L',
  95.                 $this->localPort . ':' . $bindHost . ':' . $bindPort,
  96.                 '-p',
  97.                 $sshPort,
  98.                 $sshUsername . '@' . $sshHost
  99.             ]
  100.         );
  101.  
  102.         if (!empty($ncPath)) {
  103.             $this->verifyCommand = sprintf(
  104.                 '%s -vz %s %d',
  105.                 escapeshellcmd($ncPath),
  106.                 escapeshellarg($localAddress),
  107.                 $this->localPort
  108.             );
  109.         }
  110.  
  111.         if ($autoConnect) {
  112.             $this->connect();
  113.         }
  114.     }
  115.  
  116.     /**
  117.      * Destructor. Disconnects SSH Tunnel.
  118.      */
  119.     public function __destruct()
  120.     {
  121.         if ($this->autoDisconnect) {
  122.             $this->disconnect();
  123.         }
  124.     }
  125.  
  126.     /**
  127.      * Check if the local port is available.
  128.      * @return ?bool Null when there is no method to verify.
  129.      * @throws \ErrorException
  130.      */
  131.     public function isLocalPortAvailable()
  132.     {
  133.         if (empty($this->lsofCommand)) {
  134.             return null;
  135.         }
  136.         $exitCode = $this->runCommand(
  137.             sprintf($this->lsofCommand, $this->localPort),
  138.             [1 => ['file', '/dev/null', 'w']]
  139.         );
  140.         return (1 === $exitCode);
  141.     }
  142.  
  143.     /**
  144.      * Connect SSH tunnel.
  145.      * @return bool
  146.      * @throws \ErrorException
  147.      */
  148.     public function connect()
  149.     {
  150.         // Verify first. If there is already a working tunnel that's OK.
  151.         if (true === $this->verifyTunnel()) {
  152.             return true;
  153.         }
  154.  
  155.         if (false === $this->isLocalPortAvailable()) {
  156.             throw new ErrorException(
  157.                 sprintf(
  158.                     "Local port %d is not available.\nVerified with: %s",
  159.                     $this->localPort,
  160.                     sprintf($this->lsofCommand, $this->localPort)
  161.                 )
  162.             );
  163.         }
  164.  
  165.         $this->sshProcess = $this->openProcess($this->sshCommand);
  166.  
  167.         // Ensure we wait long enough for it to actually connect.
  168.         usleep($this->waitMs);
  169.  
  170.         for ($i = 0; $i < $this->tries; $i++) {
  171.             if (false !== $this->verifyTunnel()) {
  172.                 return true;
  173.             }
  174.             // Wait a bit until next iteration
  175.             usleep($this->waitMs);
  176.         }
  177.  
  178.         throw new ErrorException(
  179.             sprintf(
  180.                 "SSH tunnel is not working.\nCreated with: %s\nVerified with: %s",
  181.                 implode(' ', $this->sshCommand),
  182.                 $this->verifyCommand
  183.             )
  184.         );
  185.     }
  186.  
  187.     /**
  188.      * Disconnect SSH tunnel.
  189.      * @return bool True if successful.
  190.      */
  191.     public function disconnect()
  192.     {
  193.         if (!empty($this->sshProcess['proc'])) {
  194.             $this->closeProcess($this->sshProcess, true);
  195.             return true;
  196.         }
  197.         return false;
  198.     }
  199.  
  200.     /**
  201.      * Verifies whether the tunnel is active or not.
  202.      * @return ?bool Null when there is no method to verify.
  203.      * @throws \ErrorException
  204.      */
  205.     public function verifyTunnel()
  206.     {
  207.         if (empty($this->verifyCommand)) {
  208.             return null;
  209.         }
  210.         return (0 === $this->runCommand($this->verifyCommand, [2 => ['file', '/dev/null', 'w']]));
  211.     }
  212.  
  213.     /**
  214.      * @param string|array<string> $command
  215.      * @param array<resource|array<string>> $descriptorSpec
  216.      * @return array<resource|array<resource>>
  217.      * @throws \ErrorException
  218.      */
  219.     protected function openProcess(
  220.         $command,
  221.         $descriptorSpec = []
  222.     ) {
  223.         if (is_array($command)) {
  224.             $command = implode(' ', $command);
  225.         }
  226.         $result = [
  227.             'pipes' => []
  228.         ];
  229.         $result['proc'] = proc_open(
  230.             $command,
  231.             $descriptorSpec,
  232.             $result['pipes']
  233.         );
  234.         if (!is_resource($result['proc'])) {
  235.             throw new ErrorException(sprintf("Error executing command: %s", $command));
  236.         }
  237.         $procStatus = proc_get_status($result['proc']);
  238.         if (!$procStatus['running'] || !$procStatus['pid']) {
  239.             throw new ErrorException(sprintf("Process is not running. Command: %s", $command));
  240.         }
  241.         return $result;
  242.     }
  243.  
  244.     /**
  245.      * Close a process opened by openProcess()
  246.      * @param array<resource|array<resource>> $process
  247.      * @param bool $kill
  248.      * @return int
  249.      */
  250.     protected function closeProcess( $process, $kill = false)
  251.     {
  252.         foreach ($process['pipes'] as $pipe) {
  253.             fclose($pipe);
  254.         }
  255.         if ($kill) {
  256.             proc_terminate($process['proc']);
  257.             proc_terminate($process['proc'], 9);
  258.         }
  259.         return proc_close($process['proc']);
  260.     }
  261.  
  262.     /**
  263.      * Runs a command, returns exit code.
  264.      * @param string $command
  265.      * @param array<resource|array<string>> $descriptorSpec
  266.      * @return int 0 means Success.
  267.      * @throws \ErrorException
  268.      */
  269.     protected function runCommand(
  270.          $command,
  271.          $descriptorSpec = []
  272.     ) {
  273.         return $this->closeProcess($this->openProcess($command, $descriptorSpec));
  274.     }
  275. }
  276.  
  277. $tunnel = new SshTunnel(
  278.     'fly',
  279.     '103.209.159.190',
  280.     22,
  281.     '127.0.0.1',
  282.     0,
  283.     '127.0.0.1',
  284.     3306,
  285.     __DIR__ . '/../../pdbsshkey'
  286. );
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement