PHPackages                             williameggers/reactphp-ssh-server - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Queues &amp; Workers](/categories/queues)
4. /
5. williameggers/reactphp-ssh-server

ActiveLibrary[Queues &amp; Workers](/categories/queues)

williameggers/reactphp-ssh-server
=================================

SSH server implementation for ReactPHP

1.1.0(1mo ago)617411BSD-2-ClausePHPPHP &gt;=8.2CI passing

Since Jul 2Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/williameggers/reactphp-ssh-server)[ Packagist](https://packagist.org/packages/williameggers/reactphp-ssh-server)[ RSS](/packages/williameggers-reactphp-ssh-server/feed)WikiDiscussions master Synced today

READMEChangelog (7)Dependencies (42)Versions (9)Used By (1)

SSH server for ReactPHP
=======================

[](#ssh-server-for-reactphp)

[![CI status](https://github.com/williameggers/reactphp-ssh/workflows/CI/badge.svg)](https://github.com/williameggers/reactphp-ssh/actions)[![Latest Stable Version](https://camo.githubusercontent.com/3085ed7bd7ae6c55c1b42d4657cb8e0652d30f0c84796034aaaded16884ff8c0/687474703a2f2f706f7365722e707567782e6f72672f77696c6c69616d6567676572732f72656163747068702d7373682d7365727665722f76)](https://packagist.org/packages/williameggers/reactphp-ssh-server)[![License](https://camo.githubusercontent.com/62e26db48f68cbc0887c3fe0a328a6802852e933513d6d2ba25fb3f08c9df593/687474703a2f2f706f7365722e707567782e6f72672f77696c6c69616d6567676572732f72656163747068702d7373682d7365727665722f6c6963656e7365)](https://packagist.org/packages/williameggers/reactphp-ssh-server)

This project is an event-driven, standalone SSH server implementation for [ReactPHP](https://reactphp.org/) developed by William Eggers. It extends the code from Ashley Hindle’s excellent [Whisp PHP SSH server](https://github.com/WhispPHP/whisp), with substantial modifications and refactoring to suit ReactPHP.

> Looking for an SSH client? Check out [williameggers/reactphp-ssh-client](https://github.com/williameggers/reactphp-ssh-client).

Overview
--------

[](#overview)

- Implements core SSH protocol functionality, including transport negotiation, authentication, and channel lifecycle management
- Built on a fully asynchronous, non-blocking architecture using ReactPHP for high concurrency and performance
- Simple to configure and extend with a clean event-driven API
- Customizable authentication and session behavior via intuitive callback handlers
- Supports SSH TCP forwarding when explicitly enabled: local forwarding via `direct-tcpip` and remote forwarding via `tcpip-forward`, `cancel-tcpip-forward`, and server-initiated `forwarded-tcpip` channels
- Supports widely-used encryption modes such as Galois/Counter Mode (GCM) and Counter Mode (CTR), along with modern host key algorithms including ssh-ed25519.
- Designed for testing, development, and internal tooling - **not security-hardened for exposure on public networks**. See [disclaimer](#disclaimer).
- Easily integrates into existing PHP applications or service layers requiring embedded SSH support
- Lightweight and dependency-friendly, making it ideal for microservices or containerized environments

**Table of Contents**

- [Disclaimer](#disclaimer)
- [Installation](#installation)
- [Quickstart example](#quickstart-example)
- [Server usage](#server-usage)
    - [Server](#server)
        - [Events](#server-events)
        - [Methods](#server-methods)
- [Connection usage](#connection-usage)
    - [Connection class](#connection-class)
        - [Events](#connection-events)
        - [Remote forwarding example](#remote-forwarding-example)
        - [Methods](#connection-methods)
    - [Channel class](#channel-class)
        - [Events](#channel-events)
        - [Methods](#channel-methods)
- [Supported algorithms](#supported-algorithms)
- [Unsupported features](#unsupported-features)
- [Contributions](#contributions)
- [License](#license)
- [Support and Credits](#support-and-credits)

Disclaimer
----------

[](#disclaimer)

This project is intended for educational, testing, and internal tooling use. It is not security-audited or recommended for exposure to untrusted clients or networks without significant review and hardening.

If you are looking for an SSH server with thorough testing and code audits to integrate with your PHP code, we recommend that you look into the OpenSSH project.

In no event shall the authors of reactphp-ssh-server be liable for anything that happens while using this library. Please read the [license](#license) for the full disclaimer.

Installation
------------

[](#installation)

Install via Composer:

```
composer require williameggers/reactphp-ssh-server
```

This library requires **PHP 8.2 or higher** and the following PHP extensions:

- **ext-sodium** - for cryptographic operations (e.g. Ed25519, Curve25519)
- **ext-mbstring** - for multibyte string handling
- **ext-openssl** - for RSA and AES encryption support

Ensure these extensions are enabled in your environment before installing the package.

Quickstart example
------------------

[](#quickstart-example)

Here is an SSH server that establishes a channel via a shell request, then closes the connection if you send it anything:

```
use React\Promise\Deferred;
use WilliamEggers\React\SSH\Server;
use WilliamEggers\React\SSH\Connection;
use WilliamEggers\React\SSH\Channel;

$server = new Server('127.0.0.1:2222');

$server->on('connection', function (Connection $connection) {
    /*
     * Handle a new SSH channel request (typically a shell).
     * This is where application-level interaction begins after authentication.
     * Use the Channel object to read from and write to the client.
     */
    $connection->on('channel.open', function (Channel $channel) {
        $channel->on('shell-request', function (Deferred $started) use ($channel) {
            $started->resolve(true);

            $channel->write("Hello " . $channel->getConnection()->getRemoteAddress() . "!\r\n");
            $channel->write("Welcome to this amazing SSH server!\r\n");
            $channel->write("Here's a tip: don't say anything.\r\n");

            $channel->on('data', function ($data) use ($channel) {
                $channel->getConnection()->close();
            });
        });
    });
});
```

See also the [examples](examples).

Server usage
------------

[](#server-usage)

### Server

[](#server)

The `Server` is responsible for providing an interface for accepting incoming SSH connections.

Besides defining a few methods, this interface also implements the [`EventEmitterInterface`](https://github.com/igorw/evenement)which allows you to react to certain events.

In order to accept SSH connections, you can simply pass a host and port combination like this:

```
$server = new WilliamEggers\React\SSH\Server('127.0.0.1:2222');
```

Listening on the localhost address 127.0.0.1 means it will not be reachable from outside of this system. In order to change the host the socket is listening on, you can provide an IP address of an interface or use the special 0.0.0.0 address to listen on all interfaces:

```
$server = new WilliamEggers\React\SSH\Server('0.0.0.0:2222');
```

If you want to listen on an IPv6 address, you MUST enclose the host in square brackets:

```
$server = new WilliamEggers\React\SSH\Server('[::1]:2222');
```

In order to use a random port assignment, you can use the port 0:

```
$server = new WilliamEggers\React\SSH\Server('127.0.0.1:0');
$address = $server->getAddress();
```

You can also specify a port number on its own, in which case the server will default to listening on the localhost address 127.0.0.1, which means it will not be reachable from outside of this system.

```
$server = new WilliamEggers\React\SSH\Server(2222);
```

To override the inactivity timeout, specify it as the second parameter. In this example we start a server on localhost (127.0.0.1) on port 2222 with a connection inactivity timeout of 300 seconds.

```
$server = new WilliamEggers\React\SSH\Server('127.0.0.1:2222', 300);
```

Decorators like `React\Socket\LimitingServer` are compatible. In the example below the number of concurrent SSH connections will be limited to 10:

```
$server = new React\Socket\LimitingServer(
    new WilliamEggers\React\SSH\Server(2222),
    10
);

$server->on('connection', function (Connection $connection) {
    ...
});
```

Important

If the given URI appears to be valid, but listening on it fails (such as if port is already in use or port below 1024 may require root access etc.), it will throw a `RuntimeException`.

#### Server Events

[](#server-events)

- **connection** - emitted when a new connection has been established, i.e. a new client connects to the server:

    ```
    $server->on('connection', function (WilliamEggers\React\SSH\Connection $connection) {
        echo 'new connection' . PHP_EOL;
    });
    ```
- **error** - emitted when there's an error accepting a new connection from a client.

    ```
    $server->on('error', function (Exception $e) {
        echo 'error: ' . $e->getMessage() . PHP_EOL;
    });
    ```

#### Server Methods

[](#server-methods)

- **setBanner(?string $banner): self**

    Sets the optional SSH authentication banner that is sent to clients before authentication begins. This can be used to display legal notices, welcome messages, or other information. Pass null to disable the banner. Returns the server instance for chaining.
- **enableAuthentication(): self**

    Enables user authentication on incoming SSH connections. When enabled, the ***authenticate*** event will be emitted during the connection handshake to allow the server to validate credentials. It returns the server instance for chaining.

    > **Important**
    >
    > Authentication is disabled by default.
- **disableAuthentication(): self**

    Disables user authentication. When disabled, all connections are considered authenticated automatically, and the authenticate event will not be emitted. This is useful for development or internal services, or services that provide an alternate authentication approach. Returns the server instance for chaining.
- **enableRemoteForwarding(): self**

    Enables remote TCP forwarding support for new SSH connections. Remote forwarding is disabled by default. Even when enabled, each `tcpip-forward` request must still be explicitly approved by handling the `global-request.tcpip-forward` event.
- **enableDirectTcpip(): self**

    Enables direct TCP/IP forwarding support for new SSH connections. Direct TCP/IP forwarding is disabled by default. Even when enabled, each `direct-tcpip` channel open request must still be explicitly approved by handling the `direct-tcpip` event.
- **disableDirectTcpip(): self**

    Disables direct TCP/IP forwarding support for new SSH connections.
- **disableRemoteForwarding(): self**

    Disables remote TCP forwarding support for new SSH connections.
- **addServerHostKey(ServerHostKey $hostKey): self**

    Registers an additional server host key that may be advertised during key exchange. This allows you to supply your own host key material or extend the default host key set.
- **getAddress(): ?string**

    Returns the full listening address for the server socket, such as `tcp://127.0.0.1:2222`.
- **setLogger(Psr\\Log\\LoggerInterface $logger): self**

    Sets the PSR-3 logger used by the server and propagated to new connections.
- **pause(): void**

    Temporarily stops accepting new incoming connections without closing the listening socket.
- **resume(): void**

    Resumes accepting new incoming connections after a previous `pause()`.
- **close(): void**

    Stops listening for new incoming connections and closes the server socket.

Connection usage
----------------

[](#connection-usage)

### Connection class

[](#connection-class)

The `Connection` class represents an individual incoming SSH connection and is emitted by the `Server` when a client connects.

It is responsible for managing the SSH protocol flow, including authentication (if enabled), channel negotiation, and connection closure. The `Connection` class implements the [`EventEmitterInterface`](https://github.com/igorw/evenement), enabling it to emit and respond to protocol-level events.

Important

Whilst `Connection` implements `DuplexStreamInterface` for internal protocol handling, **you should not read from or write to it directly**. Instead, interaction with the SSH client should occur via `Channel` objects, which are emitted during the **channel.open** event. The `Channel` encapsulates a virtual stream over which actual data is exchanged after authentication. [See RFC 4254 for further information.](https://www.rfc-editor.org/rfc/rfc4254#section-5)

The `Connection` object also exposes additional metadata such as the client's IP address ([getRemoteAddress()](#getremoteaddress)) and the server's local binding address ([getLocalAddress()](#getlocaladdress)).

Because the `Connection` implements the underlying [ConnectionInterface](https://reactphp.org/socket/#connectioninterface) you can use any of its events and methods as usual.

```
$connection->on('error', function (Exception $e) {
    echo 'error: ' . $e->getMessage();
});

$connection->on('close', function () {
    echo 'closed';
});

$connection->close();
// …
```

#### Connection Events

[](#connection-events)

- **authenticate** - emitted when the client attempts to authenticate.

    This event allows the application to determine whether the provided credentials are valid. You are free to implement any logic here - from checking a hardcoded password to querying a database or invoking an external authentication service.

    The server must resolve the provided Deferred `$authenticated` with a boolean value (***true*** to accept, ***false*** to reject).

    > **Important**
    >
    > The authenticate event is only emitted if authentication is explicitly enabled by calling `enableAuthentication()` on the `Server` instance. **Authentication is disabled by default.**
    >
    > If authentication is enabled, the deferred promise must be resolved within `Connection->deferredEventPromiseTimeout` seconds or authentication will be rejected.

    **Signature:** `function (string $username, string $method, array $credentials, Deferred $authenticated): void`

    **Callback parameters:**

    - `string $username` - The username provided by the client.
    - `string $method` - The authentication method requested (e.g., 'password', 'publickey', 'keyboard-interactive').
    - `array $credentials` - The authentication credentials, such as a password or public key blob. The meaning depends on the method.
    - `React\Promise\Deferred $authenticated` - A promise that must be resolved with true (to accept) or false (to reject) the authentication attempt.

    **Example**:

    ```
    $connection->on('authenticate', function (string $username, string $method, array $credentials, Deferred $authenticated) {
        $isAuthenticated = false;
        $password = $credentials[0] ?? null;

        if (
            'password' === $method
            && 'test' === $username
            && 'abc123' === $password
        ) {
            $isAuthenticated = true;
        }

        $authenticated->resolve($isAuthenticated);
    });
    ```
- **channel.open** - emitted when the client opens a new channel.

    This typically occurs when the client initiates a session (e.g., for executing commands, starting a shell, or setting up port forwarding). Each channel is independent and associated with a specific purpose. Most servers only handle "session" type channels, which are used for shell access and exec commands.

    You should perform all read and write operations through the provided `Channel` instance - not directly on the `Connection`.

    > **Important**
    >
    > On receipt of a **channel.open** event, the developer should typically handle the `Channel` events [**shell-request**](#channel-shell-request-event) and/or [**exec-request**](#channel-exec-request-event), as these are the most common requests.

    **Signature:** `function (Channel $channel): void`

    **Callback parameters:**

    - `Channel $channel` - The newly opened channel instance. Use this object to read input from the client, write responses, or listen to channel-specific events - see below.

    **Example:**

    ```
    $connection->on('channel.open', function (Channel $channel) {
        $channel->on('shell-request', function (Deferred $started) use ($channel) {
            $channel->write('Hello world!');
            $started->resolve(true);
        });
    });
    ```
- **channel.end** - emitted when the client sends an EOF (end-of-file) signal for a channel, indicated by an ***SSH\_MSG\_CHANNEL\_EOF*** message.

    This signals that no more data will be sent by the client on this channel, but the channel itself remains open - the server may still continue to send data until it chooses to close the channel explicitly. This event is useful for detecting when the client has finished its input stream (e.g., after running a command), but full cleanup should typically occur on channel.close.

    **Signature:** `function (Channel $channel): void`

    **Callback parameters:**

    - `Channel $channel` - The associated channel instance.
- **channel.close** - emitted when an existing channel closes.

    This signals that the client has finished all communication on that channel and expects it to be shut down. The channelId supplied by the event corresponds to the internal identifier assigned to that specific channel.

    This event is useful for cleanup or logging once a session or command has ended.

    **Signature:** `function (int $channelId): void`
- **channel.data** - emitted when raw data is received for an open channel.

    This is emitted on the `Connection` with the channel ID and payload after the data has been forwarded to the corresponding `Channel` instance.

    **Signature:** `function (int $channelId, string $data): void`
- **global-request.tcpip-forward** - emitted when the client requests remote TCP forwarding with `tcpip-forward`.

    This event is only emitted when remote forwarding has been explicitly enabled. Resolve the provided `Deferred` with `true` to allow the bind or `false` to reject it. If no listener is registered, the request is rejected.

    **Signature:** `function (string $bindAddress, int $bindPort, Deferred $allowed): void`
- **global-request.cancel-tcpip-forward** - emitted when the client requests cancellation of a remote TCP forward.

    **Signature:** `function (string $bindAddress, int $bindPort): void`
- **forwarded-tcpip.connection** - emitted when the server-side forwarded listener accepts a TCP connection and is about to open a `forwarded-tcpip` channel back to the SSH client.

    **Signature:** `function (array $info, React\Socket\ConnectionInterface $socket): void`
- **direct-tcpip** - emitted when the client requests local TCP forwarding via a `direct-tcpip` channel open.

    This event is only emitted when direct TCP/IP forwarding has been explicitly enabled. Resolve the provided `Deferred` with `true` to allow the outbound TCP connection attempt or `false` to reject it. If no listener is registered, the request is rejected.

    **Signature:** `function (array $info, Deferred $allowed): void`
- **close** - emitted when the connection is terminated.

    **Signature:** `function (): void`

#### Remote Forwarding Example

[](#remote-forwarding-example)

Remote forwarding lets an SSH client ask the server to listen on a TCP address and tunnel accepted connections back to the client over `forwarded-tcpip` channels.

```
use React\Promise\Deferred;
use WilliamEggers\React\SSH\Connection;
use WilliamEggers\React\SSH\Server;

$server = new Server('127.0.0.1:2222');
$server->enableRemoteForwarding();

$server->on('connection', function (Connection $connection): void {
    $connection->on('global-request.tcpip-forward', function (string $bindAddress, int $bindPort, Deferred $allowed): void {
        // Explicitly approve each requested bind.
        $allowed->resolve('127.0.0.1' === $bindAddress);
    });

    $connection->on('forwarded-tcpip.connection', function (array $info): void {
        printf(
            "Forwarded TCP connection: bind=%s:%d origin=%s:%d\n",
            $info['bindAddress'],
            $info['bindPort'],
            $info['originatorAddress'],
            $info['originatorPort']
        );
    });
});
```

Notes:

- Remote forwarding is disabled by default and must be enabled explicitly with `enableRemoteForwarding()`.
- Each `tcpip-forward` request must be explicitly approved by a `global-request.tcpip-forward` handler.
- Valid requests receive `SSH_MSG_REQUEST_SUCCESS`; invalid, unapproved, or rejected requests receive `SSH_MSG_REQUEST_FAILURE` without disconnecting the SSH session.
- Requests for port `0` are supported. The success reply includes the allocated port.
- The current implementation accepts literal IPv4 and IPv6 addresses plus common wildcard forms such as `0.0.0.0`, `::`, and `localhost`.
- Remote forwarding support covers SSH remote forwarding only. Dynamic forwarding remains unsupported.

#### Local Forwarding Example

[](#local-forwarding-example)

Local forwarding lets an SSH client ask the server to open an outbound TCP connection from the server to a requested destination over a `direct-tcpip` channel.

```
use React\Promise\Deferred;
use WilliamEggers\React\SSH\Connection;
use WilliamEggers\React\SSH\Server;

$server = new Server('127.0.0.1:2222');
$server->enableDirectTcpip();

$server->on('connection', function (Connection $connection): void {
    $connection->on('direct-tcpip', function (array $info, Deferred $allowed): void {
        $approved = $info['destinationAddress'] === '127.0.0.1'
            && in_array($info['destinationPort'], [80, 443, 3000], true);

        $allowed->resolve($approved);
    });
});
```

Notes:

- Direct TCP/IP forwarding is disabled by default and must be enabled explicitly with `enableDirectTcpip()`.
- Each `direct-tcpip` request must be explicitly approved by a `direct-tcpip` handler.
- Rejected or unapproved requests receive `SSH_MSG_CHANNEL_OPEN_FAILURE` with `ADMINISTRATIVELY_PROHIBITED`.
- Outbound socket connection failures receive `SSH_MSG_CHANNEL_OPEN_FAILURE` with `CONNECT_FAILED`.
- Direct TCP/IP forwarding covers SSH local forwarding (`ssh -L`). Dynamic forwarding (`ssh -D`) remains unsupported.

#### Connection Methods

[](#connection-methods)

- **getRemoteAddress()**

    The `getRemoteAddress(): ?string` method returns the full remote address (URI) where this connection has been established with.

    ```
    $address = $connection->getRemoteAddress();
    echo 'Connection with ' . $address . PHP_EOL;
    ```

    If the remote address can not be determined or is unknown at this time (such as after the connection has been closed), it MAY return a `NULL` value instead.

    Otherwise, it will return the full address (URI) as a string value, such as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443`. If you only want the remote IP, you may use something like this:

    ```
    $address = $connection->getRemoteAddress();
    $ip = trim(parse_url($address, PHP_URL_HOST), '[]');
    echo 'Connection with ' . $ip . PHP_EOL;
    ```
- **getLocalAddress()**

    The `getLocalAddress(): ?string` method returns the full local address (URI) where this connection has been established with.

    ```
    $address = $connection->getLocalAddress();
    echo 'Connection with ' . $address . PHP_EOL;
    ```

    If the local address can not be determined or is unknown at this time (such as after the connection has been closed), it MAY return a `NULL` value instead.

    Otherwise, it will return the full address (URI) as a string value, such as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443`.

    This method complements the [`getRemoteAddress()`](#getremoteaddress) method, so they should not be confused.

    If your `Server` instance is listening on multiple interfaces (e.g. using the address `0.0.0.0`), you can use this method to find out which interface actually accepted this connection (such as a public or local interface).

    If your system has multiple interfaces (e.g. a WAN and a LAN interface), you can use this method to find out which interface was actually used for this connection.
- **getChannel(int $channelId): ?Channel**

    Returns the active `Channel` instance for the given channel ID, or `null` if that channel does not exist or has already been closed.
- **getUsername(): ?string**

    Returns the authenticated username for this connection, if one has been established.
- **setKeyboardInteractiveConfig(KeyboardInteractiveConfig $config): self**

    Configures the prompts and instructions used for keyboard-interactive authentication on this connection.
- **setLogger(Psr\\Log\\LoggerInterface $logger): self**

    Sets the PSR-3 logger used by this connection and its internal protocol helpers.

### Channel class

[](#channel-class)

The `Channel` class represents an individual logical channel within an SSH connection. Channels are the mechanism through which application-layer communication (e.g. shell sessions, command execution, subsystems) is conducted over an SSH transport.

An instance of `Channel` is provided during a **channel.open** event on a `Connection`. All reading and writing of data at the application layer should take place through the `Channel` object - not the underlying `Connection`.

A single `Connection` can support multiple concurrent channels, allowing for operations such as multiple shell sessions, port forwarding, or subsystem requests over the same underlying connection. Each `Channel` operates independently and emits its own set of events.

In addition to channel-specific methods, the `Channel` class implements the [`EventEmitterInterface`](https://github.com/igorw/evenement), enabling it to emit and respond to protocol-level channel events.

#### Channel Events

[](#channel-events)

- **data** - emitted when the server receives data from the client on this channel.

    This event provides the raw string of data sent by the client, typically as part of an interactive shell session or during command execution. It is the primary way to receive client input, and can be used to process terminal input, capture command-line arguments, or respond interactively.

    **Signature:** `function (string $data) void`

    **Example**:

    ```
    $channel->on('data', function (string $data) use ($channel) {
        // Log or process the data received from the client
        echo 'Received data from client (' . $channel->getRecipientChannel() . '): ' . trim($data) . PHP_EOL;

        // Optionally, echo the data back to the client
        $channel->write("You said: " . $data);
    });
    ```
- **pty-request** - emitted when the client requests a pseudo-terminal (PTY) for the channel.

    This event is emitted after terminal information has been parsed and stored on the `Channel`. If no listener is registered, PTY requests are accepted automatically.

    **Signature:** `function (Deferred $started): void`
- **exec-request** - emitted when the client sends an exec request to run a single command (e.g., `ssh user@host ls -la`).

    The server should handle this by running the requested command and writing the output to the channel.

    How the command is interpreted or executed is left entirely up to the developer - this event is free-form by design, allowing integration with custom application logic, scripting environments, or sandboxed interpreters as needed.

    The server must resolve the provided Deferred `$started` with a boolean value (***true*** to send ***CHANNEL\_SUCCESS*** to the client, ***false*** to send ***CHANNEL\_FAILURE***).

    > **Important**
    >
    > The deferred promise must be resolved within `Connection->deferredEventPromiseTimeout` seconds or ***CHANNEL\_FAILURE*** will be sent.
    >
    > **Data queuing:** When the client expects a reply (typical for exec requests), data written to the channel before resolving the `$started` Deferred will be queued and sent only after the promise resolves. To send data immediately, resolve the Deferred as early as possible.

    **Signature:** `function (string $command, Deferred $started): void`

    **Callback parameters:**

    - `string $command` - The exact command string requested by the client.
    - `Deferred $started` - A ReactPHP Deferred promise to resolve when the command has started. Resolve with true if the command was accepted and executed, or false to indicate failure.
- **shell-request** - emitted when the client requests an interactive shell session (e.g., launching a terminal after connecting).

    Typically, this means the server should spawn an interactive shell loop or similar REPL-style interface. Again, how this event is interpreted or executed is left entirely up to the developer.

    The server must resolve the provided Deferred `$started` with a boolean value (***true*** to send ***CHANNEL\_SUCCESS*** to the client, ***false*** to send ***CHANNEL\_FAILURE***).

    > **Important**
    >
    > The deferred promise must be resolved within `Connection->deferredEventPromiseTimeout` seconds or ***CHANNEL\_FAILURE*** will be sent.
    >
    > **Data queuing:** When the client expects a reply (typical for interactive shells), data written to the channel before resolving the `$started` Deferred will be queued and sent only after the promise resolves. To send data immediately, resolve the Deferred as early as possible.

    **Signature:** `function (Deferred $started): void`

    **Callback parameters:**

    - `Deferred $started` - A ReactPHP Deferred promise to resolve when shell handling is complete. Resolve with true if the shell request was accepted and successfully executed, or false to indicate failure.
- **signal** - emitted when the client sends a POSIX-style signal (e.g., SIGINT, SIGTERM) over the channel. This is often used to interrupt a long-running command or to cancel a session gracefully.

    **Signature:** `function (string $signalName) void`
- **window-change** - emitted when the client reports a change in its terminal dimensions (rows, columns, and optionally pixel sizes).

    Useful for adjusting the display or terminal layout in a shell session or terminal emulator backend - for example, resizing a pseudo-terminal or updating the UI in a text-based application.

    This event is functionally equivalent to the **NAWS (Negotiate About Window Size)** option in Telnet, allowing the server to respond to terminal size changes initiated by the client.

    **Signature:** `function (Values\WinSize $windowSize) void`

#### Channel Methods

[](#channel-methods)

- **close()**

    Closes this SSH channel from the server side by sending `SSH_MSG_CHANNEL_CLOSE` to the client. This affects only the channel, not the underlying SSH connection.
- **getEnvironmentVariables(): array**

    Returns an associative array of environment variables that the client has requested to set for the session. This data is typically sent using SSH\_MSG\_CHANNEL\_REQUEST with the "env" request type.
- **getEnvironmentVariable(string $name): mixed**

    Retrieves the value of a specific environment variable sent by the client. If the variable was not provided, it returns null.
- **getEncoding(): string**

    Resolves the character encoding requested by the client. This method inspects standard locale-related environment variables (LC\_ALL, LC\_CTYPE, LANG) in order of precedence to extract the character encoding portion from values like "en\_US.UTF-8".

    The detected encoding will be returned in lowercase (e.g., "utf-8", "cp437")

    If no valid encoding is found, it defaults to "utf-8".
- **getTerminalInfo(): ?TerminalInfo**

    Returns an instance of [`TerminalInfo`](https://github.com/williameggers/reactphp-ssh/blob/master/src/Values/TerminalInfo.php) if the client has requested a pseudo-terminal (PTY) during session setup. This includes details such as terminal type, dimensions, and terminal mode flags. Returns null if no PTY was requested.
- **getRecipientChannel(): int**

    Returns the channel ID assigned by the client for this channel.
- **getSenderChannel(): int**

    Returns the channel ID assigned by the server for this channel.
- **getWindowSize(): int**

    Returns the current channel window size in bytes.
- **getMaxPacketSize(): int**

    Returns the maximum packet size for this channel.
- **write($data)**

    Sends a string of data back to the client over the channel using SSH\_MSG\_CHANNEL\_DATA. This is the primary method for server-side output in shell sessions, exec commands, or other interactive flows.

    > **Note:** When the channel is in a pending request state (e.g., before the shell-request or exec-request Deferred is resolved), written data may be queued until the request completes.
- **end($data = null)**

    Optionally sends final data, then ends this SSH channel by sending `SSH_MSG_CHANNEL_EOF` followed by `SSH_MSG_CHANNEL_CLOSE`. This affects only the channel, not the underlying SSH connection.
- **pause(): void**

    Pauses reading application data from this channel.
- **resume(): void**

    Resumes reading application data from this channel.
- **isReadable(): bool**

    Indicates whether the channel can currently emit application data.
- **isWritable(): bool**

    Indicates whether the channel can currently send data to the client.

Supported algorithms
--------------------

[](#supported-algorithms)

The negotiated algorithm lists are defined in `KexNegotiator`.

### Key exchange methods

[](#key-exchange-methods)

The following key exchange methods are supported:

- curve25519-sha256
-
- diffie-hellman-group14-sha256

### Server host key algorithms

[](#server-host-key-algorithms)

The following server host key algorithms are supported:

- ssh-ed25519
- rsa-sha2-256
- rsa-sha2-512

### Encryption algorithms

[](#encryption-algorithms)

The following encryption algorithms are supported:

- aes128-ctr
-
- aes192-ctr
- aes256-ctr
-

### MAC algorithms

[](#mac-algorithms)

The following MAC algorithms are supported:

- hmac-sha2-256
- hmac-sha2-512
- hmac-sha1

Unsupported features
--------------------

[](#unsupported-features)

This SSH server implementation is intentionally scoped to support core SSH protocol functionality such as connection management, authentication, and basic channel operations. The following features are not supported and are unlikely to be implemented in the future, as they fall outside the intended use cases of this project (e.g. testing, embedded tools, or controlled environments):

- `ssh-copy-id` **capability** - This server does not support automatically installing public keys via the ssh-copy-id tool. Public key management must be handled externally.
- **Dynamic forwarding** - Dynamic (`-D`) forwarding is not supported. Local (`-L`) forwarding via `direct-tcpip` and remote (`-R`) forwarding via `tcpip-forward` / `cancel-tcpip-forward` are supported only when explicitly enabled and approved per request.
- **Proxy functionality** - Acting as an SSH proxy or jump host is not supported, and this server will not relay SSH traffic between clients or other SSH servers.
- **SFTP subsystem** - File transfer via the SSH File Transfer Protocol (SFTP) is not implemented, and this server will not respond to SFTP subsystem requests.

Contributions
-------------

[](#contributions)

Contributions are welcome and encouraged!

To contribute:

1. Fork the repository.
2. Create a new branch for your changes.
3. Submit a pull request with a clear description of what you've done and why.

Please try to follow existing coding style and conventions, and include tests if applicable. Feel free to open an issue if you'd like to discuss a potential change or need guidance on where to start.

License
-------

[](#license)

BSD 2-Clause License

Copyright (c) 2025, William Eggers

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

---

Portions of this software are derived from the Whisp PHP SSH server () by Ashley Hindle, and are used under the terms of the MIT License:

MIT License

Copyright (c) 2023 Ashley Hindle

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Support and Credits
-------------------

[](#support-and-credits)

This project is a derivative work based on Whisp PHP SSH by Ashley Hindle, and incorporates substantial modifications and adaptations. Whilst inspired by the original implementation, the codebase has been extensively modified and extended to support additional algorithms and features.

Licensed under the MIT License Whisp provided the core implementation of SSH transport, packet handling, and protocol logic. This project builds on that foundation with modifications suitable for ReactPHP compatibility and extended use cases.

If you are using both sides of the stack, see also [`williameggers/reactphp-ssh-client`](https://github.com/williameggers/reactphp-ssh-client).

###  Health Score

47

—

FairBetter than 93% of packages

Maintenance90

Actively maintained with recent releases

Popularity21

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity55

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~52 days

Recently: every ~62 days

Total

7

Last Release

53d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/6848430?v=4)[William Eggers](/maintainers/williameggers)[@williameggers](https://github.com/williameggers)

---

Top Contributors

[![williameggers](https://avatars.githubusercontent.com/u/6848430?v=4)](https://github.com/williameggers "williameggers (12 commits)")

---

Tags

asyncsshreactphpserver

###  Code Quality

TestsPest

Static AnalysisPHPStan, Rector

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/williameggers-reactphp-ssh-server/health.svg)

```
[![Health](https://phpackages.com/badges/williameggers-reactphp-ssh-server/health.svg)](https://phpackages.com/packages/williameggers-reactphp-ssh-server)
```

###  Alternatives

[ccxt/ccxt

A cryptocurrency trading API with more than 100 exchanges in JavaScript / TypeScript / Python / C# / PHP / Go

43.2k341.0k1](/packages/ccxt-ccxt)[react/socket

Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP

1.3k135.6M458](/packages/react-socket)[rector/rector-src

Instant Upgrade and Automated Refactoring of any PHP code

136406.3k14](/packages/rector-rector-src)[clue/docker-react

Async, event-driven access to the Docker Engine API, built on top of ReactPHP.

113163.3k1](/packages/clue-docker-react)[react/http

Event-driven, streaming HTTP client and server implementation for ReactPHP

78127.7M466](/packages/react-http)[react/datagram

Event-driven UDP datagram socket client and server for ReactPHP

99868.6k45](/packages/react-datagram)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
