websh

package
v1.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 15, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var WebshCmd = &cobra.Command{
	Use:   "websh [flags] [USER@]SERVER [COMMAND]",
	Short: "Open a websh terminal or execute a command on a server",
	Long: `Open a websh terminal for interacting with a server or execute a command directly on the server.
Supports SSH-like user@host syntax for specifying the username inline.
For executing commands, it is highly recommended to wrap the entire command string in quotes
to ensure it is interpreted correctly on the remote server.

Shell metacharacters (;, |, &, $) pass through unquoted to the remote shell.
To send a literal metacharacter, wrap the argument in quotes:
  alpacon websh server 'echo hello;world'

Executing a command (SERVER followed by COMMAND) runs through the same executor
as 'alpacon exec'—identical output, exit codes, and sudo-approval handling.
Command execution does not accept exec-only flags such as --detach or --wait.

Exit code 3 indicates a WorkSession gate denial; run with --output json to
parse a machine-readable diagnostic on stderr.
Exit code 4 indicates the sudo command is pending human approval; approve it in
the Alpacon console (web), then re-run—or use 'alpacon exec --wait' to block.
Requires an active WorkSession when using Browser login (Auth0); Token auth (API token or Service token) bypasses this requirement.`,
	Example: `  # Open a websh terminal
  alpacon websh my-server

  # Open as root using SSH-like syntax
  alpacon websh root@my-server

  # Open with specific user and group
  alpacon websh admin@my-server
  alpacon websh -u admin -g developers my-server

  # Execute a command on a server
  alpacon websh my-server "ls -la /var/log"
  alpacon websh root@my-server "systemctl status nginx"

  # Pass a secret via the shell env; the value stays off the alpacon command line
  # and out of the session recording. psql reads PGPASSWORD directly from the env.
  export PGPASSWORD=hunter2
  alpacon websh --env="PGPASSWORD" my-server 'psql -h localhost -U app -c "SELECT 1"'

  # Share terminal session
  alpacon websh --share my-server
  alpacon websh --share --read-only=true my-server

  # Join an existing shared session
  alpacon websh join --url https://myws.us1.alpacon.io/websh/shared/abcd1234?channel=default --password my-session-pass

  # Session management
  alpacon websh ls                          # List active sessions
  alpacon websh describe SESSION_ID         # Show session details
  alpacon websh watch SESSION_ID            # Watch a session (read-only, staff/superuser only)
  alpacon websh invite SESSION_ID --email user@example.com
  alpacon websh close SESSION_ID            # Close a session
  alpacon websh force-close SESSION_ID      # Force close (admin only)

Flags:
  -u, --username [USER_NAME]         Specify the username for command execution.
  -g, --groupname [GROUP_NAME]       Specify the group name for command execution.
  --env="KEY"                        Pass an environment variable, reading its value
                                     from the current shell. This keeps the value off
                                     the alpacon command line and out of the audit
                                     log—use it for secrets such as passwords or tokens.
  --env="KEY=VALUE"                  Set 'KEY' to a literal value. Discouraged: the
                                     value is written on the command line and recorded
                                     verbatim in the audit log. Never pass credentials
                                     this way.
  -s, --share                        Share the terminal via a temporary link.
  --read-only=[true|false]           Set shared session to read-only (default: false).
  --work-session [UUID]              Attach this session to a work-session.
                                     Overrides the workspace's active session
                                     set via 'alpacon work-session use'.

Note: All flags must be placed before the server name.
      Everything after the server name is treated as the remote command.`,

	DisableFlagParsing: true,
	Run: func(cmd *cobra.Command, args []string) {
		parsed, err := ParseWebshArgs(args)
		if err != nil {
			if errors.Is(err, errHelpRequested) {
				_ = cmd.Help()
				return
			}
			utils.CliErrorWithExit("%s", err)
		}

		username := parsed.Username
		groupname := parsed.Groupname
		serverName := parsed.ServerName
		commandArgs := parsed.CommandArgs
		share := parsed.Share
		readOnly := parsed.ReadOnly
		env := parsed.Env

		if serverName == "" {
			utils.CliErrorWithExit("Server name is required.")
		}

		if share && len(commandArgs) > 0 {
			utils.CliErrorWithExit("The --share flag cannot be used with remote commands. Use --share for interactive sessions only.")
		}

		if strings.Contains(serverName, "@") && !strings.Contains(serverName, ":") {
			sshTarget := utils.ParseSSHTarget(serverName)
			if username == "" && sshTarget.User != "" {
				username = sshTarget.User
			}
			serverName = sshTarget.Host
		}

		if len(commandArgs) > 0 {
			execCmd.RunRemoteExec(execCmd.RemoteExecArgs{
				Username:      username,
				Groupname:     groupname,
				WorkSessionID: parsed.WorkSessionID,
				OutputFormat:  parsed.OutputFormat,
				Server:        serverName,
				Command:       execCmd.ShellJoin(commandArgs),
				Env:           env,
			})
			return
		}

		if len(env) > 0 {
			utils.CliWarning("--env has no effect on an interactive websh session; it applies only when running a command")
		}

		if parsed.OutputFormat != "" {
			if parsed.OutputFormat != utils.OutputFormatTable && parsed.OutputFormat != utils.OutputFormatJSON {
				utils.CliErrorWithExit("invalid --output value %q: must be 'table' or 'json'", parsed.OutputFormat)
			}
			utils.OutputFormat = parsed.OutputFormat
		}

		workSessionID := worksession.ResolveOrExit(parsed.WorkSessionID)

		authMethod := config.ResolveAuthMethod()

		alpaconClient, err := client.NewAlpaconAPIClient()
		if err != nil {
			utils.CliErrorWithExit("Connection to Alpacon API failed: %s. Consider re-logging.", err)
		}

		session, err := websh.CreateWebshSession(alpaconClient, serverName, username, groupname, share, readOnly, workSessionID)

		if err != nil {
			err = utils.HandleCommonErrors(err, serverName, utils.ErrorHandlerCallbacks{
				OnMFARequired: func(srv string) error {
					return mfa.HandleMFAError(alpaconClient, srv)
				},
				OnUsernameRequired: func() error {
					_, err := iam.HandleUsernameRequired()
					return err
				},
				CheckMFACompleted: func() (bool, error) {
					return mfa.CheckMFACompletion(alpaconClient)
				},
				RefreshToken: alpaconClient.RefreshToken,
				RetryOperation: func() error {
					session, err = websh.CreateWebshSession(alpaconClient, serverName, username, groupname, share, readOnly, workSessionID)
					return err
				},
			})

			if err != nil {
				utils.HandleWorkSessionError(err, "websh", serverName, authMethod, workSessionID)
				utils.CliErrorWithExit("Failed to create websh session for '%s' server: %s.", serverName, err)
			}
		}

		listenerDone := make(chan *event.SudoListener, 1)
		go func() {
			listenerDone <- setupSudoListener(alpaconClient, session.ID, serverName)
		}()
		defer func() {
			select {
			case sl := <-listenerDone:
				if sl != nil {
					sl.Stop()
				}
			case <-time.After(3 * time.Second):

			}
		}()

		_ = websh.OpenNewTerminal(alpaconClient, session)
	},
}

Functions

This section is empty.

Types

type WebshArgs added in v1.4.6

type WebshArgs struct {
	Username      string
	Groupname     string
	ServerName    string
	CommandArgs   []string
	Share         bool
	ReadOnly      bool
	WorkSessionID string
	OutputFormat  string
	Env           map[string]string
}

func ParseWebshArgs added in v1.4.6

func ParseWebshArgs(args []string) (WebshArgs, error)

ParseWebshArgs parses raw CLI args for `alpacon websh` (DisableFlagParsing mode). Returns errHelpRequested when -h/--help is seen.

NOTE: --read-only must be checked before generic -r prefixes, and --work-session before the default fallthrough.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL