Ssh notify

use "../ssh_error"
use "../ssh_crypto"
use "../ssh_connection"
use "../ssh_auth"

interface SshClientNotify
  """
  Client-side consumer interface. The session calls these as the connection
  progresses: it must approve or reject the server host key
  (ssh_verify_host_key → session.accept_host_key/reject_host_key), is told when
  the session is ready and authenticated (ssh_ready), and receives channel and
  error events. Every callback receives the session tag so the consumer can
  drive it.
  """
  be ssh_verify_host_key(session: SshSession tag, host: String val, key: SshHostKey val)
  be ssh_ready(session: SshSession tag)
  be ssh_auth_failed(session: SshSession tag, err: SshAuthError val)
  be ssh_channel_opened(session: SshSession tag, channel_id: U32)
  be ssh_channel_request_result(session: SshSession tag, channel_id: U32,
    accepted: Bool)
    """
    Reports a peer reply to a channel request sent with want_reply set. The
    reply contains no request identifier. Wait for a result before sending
    another reply-seeking request on the same channel if its identity matters.
    """
  be ssh_channel_data(session: SshSession tag, channel_id: U32, data: Array[U8] val)
  be ssh_channel_error(session: SshSession tag, channel_id: U32, err: SshChannelError val)
  be ssh_channel_send_result(session: SshSession tag, channel_id: U32,
    data: Array[U8] val, accepted: USize, outcome: SshSendOutcome)
    """
    Reports one result for each channel_send call. accepted counts the prefix
    admitted locally. data is the original submitted array. A blocked suffix
    remains the caller's responsibility.
    """
  be ssh_channel_window_available(session: SshSession tag, channel_id: U32)
    """
    The peer granted credit after a blocked send. The hint is sent once until
    another send blocks; it does not reserve credit for a retry.
    """
  be ssh_channel_closed(session: SshSession tag, channel_id: U32)
  be ssh_error(session: SshSession tag, err: SshTransportError val)
  be ssh_disconnected(session: SshSession tag)

interface SshServerNotify
  """
  Server-side consumer interface. The session owns the SSH protocol: it sends
  every protocol reply via its own accept_*/reject_* and auth_* behaviors. A
  consumer signals a decision by calling one of those behaviors back on the
  session; it never frames protocol messages itself.

  Authentication and authorization DENY by default. The auth callback's default
  rejects unless validate_password/validate_publickey accept, and every channel
  authorization callback rejects unless overridden — there is no permissive
  default to forget. validate_password and validate_publickey have no default
  at all, so the compiler requires every server to state its auth policy.

  The SshListener that starts sessions reports its bind outcome here too:
  ssh_listener_started once it is bound and accepting, ssh_listener_failed if
  the bind failed. Start clients only after ssh_listener_started; a connect
  issued before it can be refused.
  """

  // --- Authentication policy. Consulted by the default ssh_auth_request. ---

  fun validate_password(username: String val, password: String val): Bool
    """Return true to accept this password. No default: state your policy."""

  fun validate_publickey(username: String val,
    pk: SshAuthPublicKeyData val): Bool
    """
    Return true to accept this public key. By the time a signed request reaches
    here the session has already cryptographically verified possession of the
    private key, so this only decides authorization. Use pk.matches(...) to
    compare against an authorized key. No default: state your policy.
    """

  // --- Lifecycle ---

  // The listener is an SshListener, but ssh_server imports this package, so
  // naming it here would be a package cycle. DisposableActor is the interface
  // an SshListener satisfies that this package can see.
  be ssh_listener_started(listener: DisposableActor tag) => None
    """
    The listener is bound and accepting connections. It arrives before any
    ssh_session_started from that listener. `listener` is the SshListener that
    reported, seen as a DisposableActor: compare it with `is` against the
    listeners you created, and call dispose() on it to stop listening. To
    compare, the notify must already know the listener when the callback
    arrives: construct the listener inside the notify, or record it in the
    same behaviour that constructs it.
    """
  be ssh_listener_failed(listener: DisposableActor tag) => None
    """
    The listener could not bind, so no connection will ever arrive on it.
    `listener` is the SshListener that reported, as in ssh_listener_started.
    """
  be ssh_session_started(session: SshSession tag) => None
  be ssh_session_ready(session: SshSession tag) => None

  // --- Authorization. Defaults DENY; override and call session.accept_channel
  // / session.accept_request to grant access. ---

  be ssh_channel_open_request(session: SshSession tag, channel_id: U32,
    channel_type: String val) =>
    // RFC 4254 §5.1 reason 1 = SSH_OPEN_ADMINISTRATIVELY_PROHIBITED.
    session.reject_channel(channel_id, 1)

  be ssh_pty_request(session: SshSession tag, channel_id: U32,
    pty: SshPtyState val, want_reply: Bool) =>
    if want_reply then session.reject_request(channel_id) end

  be ssh_shell_request(session: SshSession tag, channel_id: U32,
    want_reply: Bool) =>
    if want_reply then session.reject_request(channel_id) end

  be ssh_channel_request(session: SshSession tag, channel_id: U32,
    request_type: String val, want_reply: Bool) =>
    if want_reply then session.reject_request(channel_id) end

  be ssh_window_change(session: SshSession tag, channel_id: U32,
    width_chars: U32, height_rows: U32, width_pixels: U32,
    height_pixels: U32) => None

  // --- Data / events. No protocol reply expected. ---

  be ssh_channel_data(session: SshSession tag, channel_id: U32,
    data: Array[U8] val) => None
  be ssh_channel_error(session: SshSession tag, channel_id: U32,
    err: SshChannelError val) => None
  be ssh_channel_send_result(session: SshSession tag, channel_id: U32,
    data: Array[U8] val, accepted: USize, outcome: SshSendOutcome)
    """
    Reports one result for each channel_send call. accepted counts the prefix
    admitted locally. data is the original submitted array. A blocked suffix
    remains the caller's responsibility.
    """
  be ssh_channel_window_available(session: SshSession tag, channel_id: U32)
    """
    The peer granted credit after a blocked send. The hint is sent once until
    another send blocks; it does not reserve credit for a retry.
    """
  be ssh_channel_closed(session: SshSession tag, channel_id: U32) => None
  be ssh_error(session: SshSession tag, err: SshTransportError val) => None
  be ssh_disconnected(session: SshSession tag) => None

  // --- Authentication dispatch. The session has already verified possession of
  // the private key for a signed publickey request before this is called, so
  // the consumer only ever decides authorization. ---

  be ssh_auth_request(session: SshSession tag, request: SshAuthRequest val) =>
    match request.method_data
    | let pw: SshAuthPasswordData val =>
      if validate_password(request.username, pw.password) then
        session.auth_accept()
      else
        session.auth_reject(["publickey"; "password"])
      end
    | let pk: SshAuthPublicKeyData val =>
      if validate_publickey(request.username, pk) then
        match pk.signature
        | None =>
          // Probe: the client is asking whether this key is acceptable. PK_OK
          // tells it to proceed and sign; it grants no access on its own.
          session.auth_pk_ok(pk.algorithm, pk.public_key)
        | let _: Array[U8] val =>
          // Signature already verified by the session; authorization granted.
          session.auth_accept()
        end
      else
        session.auth_reject(["publickey"; "password"])
      end
    else
      session.auth_reject(["publickey"; "password"])
    end