package ssh

import (
	"crypto/subtle"
	"errors"
	"net"

	gossh "golang.org/x/crypto/ssh"
)

type Signal string

// POSIX signals as listed in RFC 4254 Section 6.10.
const (
	SIGABRT Signal = "ABRT"
	SIGALRM Signal = "ALRM"
	SIGFPE  Signal = "FPE"
	SIGHUP  Signal = "HUP"
	SIGILL  Signal = "ILL"
	SIGINT  Signal = "INT"
	SIGKILL Signal = "KILL"
	SIGPIPE Signal = "PIPE"
	SIGQUIT Signal = "QUIT"
	SIGSEGV Signal = "SEGV"
	SIGTERM Signal = "TERM"
	SIGUSR1 Signal = "USR1"
	SIGUSR2 Signal = "USR2"
)

// DefaultHandler is the default Handler used by Serve.
var DefaultHandler Handler

// ErrRejected may be returned by LocalUnixForwardingCallback or
// ReverseUnixForwardingCallback to reject a forwarding request. When
// returned, the server replies with "prohibited" rather than
// "connection failed."
var ErrRejected = errors.New("ssh: rejected")

// Option is a functional option handler for Server.
type Option func(*Server) error

// Handler is a callback for handling established SSH sessions.
type Handler func(Session)

// BannerHandler is a callback for displaying the server banner.
type BannerHandler func(ctx Context) string

// PublicKeyHandler is a callback for performing public key authentication.
type PublicKeyHandler func(ctx Context, key PublicKey) error

type NoClientAuthHandler func(ctx Context) error

// PasswordHandler is a callback for performing password authentication.
type PasswordHandler func(ctx Context, password string) bool

// KeyboardInteractiveHandler is a callback for performing keyboard-interactive authentication.
type KeyboardInteractiveHandler func(ctx Context, challenger gossh.KeyboardInteractiveChallenge) bool

// PtyCallback is a hook for allowing PTY sessions.
type PtyCallback func(ctx Context, pty Pty) bool

// SessionRequestCallback is a callback for allowing or denying SSH sessions.
type SessionRequestCallback func(sess Session, requestType string) bool

// ConnCallback is a hook for new connections before handling.
// It allows wrapping for timeouts and limiting by returning
// the net.Conn that will be used as the underlying connection.
type ConnCallback func(ctx Context, conn net.Conn) net.Conn

// LocalPortForwardingCallback is a hook for allowing port forwarding
type LocalPortForwardingCallback func(ctx Context, destinationHost string, destinationPort uint32) bool

// ReversePortForwardingCallback is a hook for allowing reverse port forwarding
type ReversePortForwardingCallback func(ctx Context, bindHost string, bindPort uint32) bool

// LocalUnixForwardingCallback is a hook for allowing unix forwarding
// (direct-streamlocal@openssh.com). The callback receives the client-requested
// socket path and returns a connection to the target socket, or an error.
//
// Returning ErrRejected (or an error wrapping it) rejects the request with
// "administratively prohibited" and the error message is sent to the client.
// Any other error rejects with "connection failed."
//
// Use NewLocalUnixForwardingCallback to create a callback with built-in path
// validation and security controls.
type LocalUnixForwardingCallback func(ctx Context, socketPath string) (net.Conn, error)

// ReverseUnixForwardingCallback is a hook for allowing reverse unix forwarding
// (streamlocal-forward@openssh.com). The callback receives the client-requested
// socket path and returns a listener bound to that path, or an error.
//
// Returning ErrRejected (or an error wrapping it) rejects the request with
// "administratively prohibited" and the error message is sent to the client.
// Any other error rejects the request silently.
//
// Use NewReverseUnixForwardingCallback to create a callback with built-in path
// validation, permission controls, and security defaults matching OpenSSH.
type ReverseUnixForwardingCallback func(ctx Context, socketPath string) (net.Listener, error)

// ServerConfigCallback is a hook for creating custom default server configs
type ServerConfigCallback func(ctx Context) *gossh.ServerConfig

// ConnectionFailedCallback is a hook for reporting failed connections
// Please note: the net.Conn is likely to be closed at this point
type ConnectionFailedCallback func(conn net.Conn, err error)

// Window represents the size of a PTY window.
//
// See https://datatracker.ietf.org/doc/html/rfc4254#section-6.2
//
// Zero dimension parameters MUST be ignored. The character/row dimensions
// override the pixel dimensions (when nonzero).  Pixel dimensions refer
// to the drawable area of the window.
type Window struct {
	// Width is the number of columns.
	// It overrides WidthPixels.
	Width int
	// Height is the number of rows.
	// It overrides HeightPixels.
	Height int

	// WidthPixels is the drawable width of the window, in pixels.
	WidthPixels int
	// HeightPixels is the drawable height of the window, in pixels.
	HeightPixels int
}

// Pty represents a PTY request and configuration.
type Pty struct {
	// Term is the TERM environment variable value.
	Term string

	// Window is the Window sent as part of the pty-req.
	Window Window

	// Modes represent a mapping of Terminal Mode opcode to value as it was
	// requested by the client as part of the pty-req. These are outlined as
	// part of https://datatracker.ietf.org/doc/html/rfc4254#section-8.
	//
	// The opcodes are defined as constants in golang.org/x/crypto/ssh (VINTR,VQUIT,etc.).
	// Boolean opcodes have values 0 or 1.
	Modes gossh.TerminalModes
}

// Serve accepts incoming SSH connections on the listener l, creating a new
// connection goroutine for each. The connection goroutines read requests and
// then calls handler to handle sessions. Handler is typically nil, in which
// case the DefaultHandler is used.
func Serve(l net.Listener, handler Handler, options ...Option) error {
	srv := &Server{Handler: handler}
	for _, option := range options {
		if err := srv.SetOption(option); err != nil {
			return err
		}
	}
	return srv.Serve(l)
}

// ListenAndServe listens on the TCP network address addr and then calls Serve
// with handler to handle sessions on incoming connections. Handler is typically
// nil, in which case the DefaultHandler is used.
func ListenAndServe(addr string, handler Handler, options ...Option) error {
	srv := &Server{Addr: addr, Handler: handler}
	for _, option := range options {
		if err := srv.SetOption(option); err != nil {
			return err
		}
	}
	return srv.ListenAndServe()
}

// Handle registers the handler as the DefaultHandler.
func Handle(handler Handler) {
	DefaultHandler = handler
}

// KeysEqual is constant time compare of the keys to avoid timing attacks.
func KeysEqual(ak, bk PublicKey) bool {
	// avoid panic if one of the keys is nil, return false instead
	if ak == nil || bk == nil {
		return false
	}

	a := ak.Marshal()
	b := bk.Marshal()
	return (len(a) == len(b) && subtle.ConstantTimeCompare(a, b) == 1)
}
