Files
runtime/apps/daemon/pkg/ssh/server.go
zeekay ea80dc3638 feat: Add new Dockerfiles and improve containerization
- Add main Dockerfile for production builds
- Add Dockerfile.runtime for secure code execution
- Add Dockerfile.runtime.fixed with improved configuration
- Include .dockerignore for build optimization
- Add integration documentation and publishing guides
- Update API and CLI components for better runtime support
- Enhance sandbox and container management features
2025-08-16 23:03:39 -05:00

258 lines
5.9 KiB
Go

// Copyright 2025 Hanzo Industries Inc.
// SPDX-License-Identifier: AGPL-3.0
package ssh
import (
"fmt"
"io"
"os"
"os/exec"
"github.com/hanzoai/daemon/pkg/common"
"github.com/hanzoai/daemon/pkg/ssh/config"
"github.com/gliderlabs/ssh"
"github.com/pkg/sftp"
"golang.org/x/sys/unix"
log "github.com/sirupsen/logrus"
)
type Server struct {
ProjectDir string
DefaultProjectDir string
}
func (s *Server) Start() error {
forwardedTCPHandler := &ssh.ForwardedTCPHandler{}
unixForwardHandler := newForwardedUnixHandler()
sshServer := ssh.Server{
Addr: fmt.Sprintf(":%d", config.SSH_PORT),
Handler: func(session ssh.Session) {
switch ss := session.Subsystem(); ss {
case "":
case "sftp":
s.sftpHandler(session)
return
default:
log.Errorf("Subsystem %s not supported\n", ss)
session.Exit(1)
return
}
ptyReq, winCh, isPty := session.Pty()
if session.RawCommand() == "" && isPty {
s.handlePty(session, ptyReq, winCh)
} else {
s.handleNonPty(session)
}
},
ChannelHandlers: map[string]ssh.ChannelHandler{
"session": ssh.DefaultSessionHandler,
"direct-tcpip": ssh.DirectTCPIPHandler,
"[email protected]": directStreamLocalHandler,
},
RequestHandlers: map[string]ssh.RequestHandler{
"tcpip-forward": forwardedTCPHandler.HandleSSHRequest,
"cancel-tcpip-forward": forwardedTCPHandler.HandleSSHRequest,
"[email protected]": unixForwardHandler.HandleSSHRequest,
"[email protected]": unixForwardHandler.HandleSSHRequest,
},
SubsystemHandlers: map[string]ssh.SubsystemHandler{
"sftp": s.sftpHandler,
},
LocalPortForwardingCallback: ssh.LocalPortForwardingCallback(func(ctx ssh.Context, dhost string, dport uint32) bool {
return true
}),
ReversePortForwardingCallback: ssh.ReversePortForwardingCallback(func(ctx ssh.Context, host string, port uint32) bool {
return true
}),
SessionRequestCallback: func(sess ssh.Session, requestType string) bool {
return true
},
}
log.Printf("Starting ssh server on port %d...\n", config.SSH_PORT)
return sshServer.ListenAndServe()
}
func (s *Server) handlePty(session ssh.Session, ptyReq ssh.Pty, winCh <-chan ssh.Window) {
dir := s.ProjectDir
if _, err := os.Stat(s.ProjectDir); os.IsNotExist(err) {
dir = s.DefaultProjectDir
}
env := []string{}
if ssh.AgentRequested(session) {
l, err := ssh.NewAgentListener()
if err != nil {
log.Errorf("Failed to start agent listener: %v", err)
return
}
defer l.Close()
go ssh.ForwardAgentConnections(l, session)
env = append(env, fmt.Sprintf("%s=%s", "SSH_AUTH_SOCK", l.Addr().String()))
}
sizeCh := make(chan common.TTYSize)
go func() {
for win := range winCh {
sizeCh <- common.TTYSize{
Height: win.Height,
Width: win.Width,
}
}
}()
err := common.SpawnTTY(common.SpawnTTYOptions{
Dir: dir,
StdIn: session,
StdOut: session,
Term: ptyReq.Term,
Env: env,
SizeCh: sizeCh,
})
if err != nil {
// Debug log here because this gets called on each ssh "exit"
// TODO: Find a better way to handle this
log.Debugf("Failed to spawn tty: %v", err)
return
}
}
func (s *Server) handleNonPty(session ssh.Session) {
args := []string{}
if len(session.Command()) > 0 {
args = append([]string{"-c"}, session.RawCommand())
}
cmd := exec.Command("/bin/sh", args...)
cmd.Env = append(cmd.Env, os.Environ()...)
if ssh.AgentRequested(session) {
l, err := ssh.NewAgentListener()
if err != nil {
log.Errorf("Failed to start agent listener: %v", err)
return
}
defer l.Close()
go ssh.ForwardAgentConnections(l, session)
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", "SSH_AUTH_SOCK", l.Addr().String()))
}
cmd.Dir = s.ProjectDir
if _, err := os.Stat(s.ProjectDir); os.IsNotExist(err) {
cmd.Dir = s.DefaultProjectDir
}
cmd.Stdout = session
cmd.Stderr = session.Stderr()
stdinPipe, err := cmd.StdinPipe()
if err != nil {
log.Errorf("Unable to setup stdin for session: %v", err)
return
}
go func() {
_, err := io.Copy(stdinPipe, session)
if err != nil {
log.Errorf("Unable to read from session: %v", err)
return
}
_ = stdinPipe.Close()
}()
err = cmd.Start()
if err != nil {
log.Errorf("Unable to start command: %v", err)
return
}
sigs := make(chan ssh.Signal, 1)
session.Signals(sigs)
defer func() {
session.Signals(nil)
close(sigs)
}()
go func() {
for sig := range sigs {
signal := s.osSignalFrom(sig)
err := cmd.Process.Signal(signal)
if err != nil {
log.Warnf("Unable to send signal to process: %v", err)
}
}
}()
err = cmd.Wait()
if err != nil {
log.Println(session.RawCommand(), " ", err)
session.Exit(127)
return
}
err = session.Exit(0)
if err != nil {
log.Warnf("Unable to exit session: %v", err)
}
}
func (s *Server) osSignalFrom(sig ssh.Signal) os.Signal {
switch sig {
case ssh.SIGABRT:
return unix.SIGABRT
case ssh.SIGALRM:
return unix.SIGALRM
case ssh.SIGFPE:
return unix.SIGFPE
case ssh.SIGHUP:
return unix.SIGHUP
case ssh.SIGILL:
return unix.SIGILL
case ssh.SIGINT:
return unix.SIGINT
case ssh.SIGKILL:
return unix.SIGKILL
case ssh.SIGPIPE:
return unix.SIGPIPE
case ssh.SIGQUIT:
return unix.SIGQUIT
case ssh.SIGSEGV:
return unix.SIGSEGV
case ssh.SIGTERM:
return unix.SIGTERM
case ssh.SIGUSR1:
return unix.SIGUSR1
case ssh.SIGUSR2:
return unix.SIGUSR2
// Unhandled, use sane fallback.
default:
return unix.SIGKILL
}
}
func (s *Server) sftpHandler(session ssh.Session) {
debugStream := io.Discard
serverOptions := []sftp.ServerOption{
sftp.WithDebug(debugStream),
}
server, err := sftp.NewServer(
session,
serverOptions...,
)
if err != nil {
log.Errorf("sftp server init error: %s\n", err)
return
}
if err := server.Serve(); err == io.EOF {
server.Close()
} else if err != nil {
log.Errorf("sftp server completed with error: %s\n", err)
}
}