-
Notifications
You must be signed in to change notification settings - Fork 120
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
03a53f1
commit 7e021b4
Showing
4 changed files
with
277 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
-----BEGIN OPENSSH PRIVATE KEY----- | ||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW | ||
QyNTUxOQAAACAPsAgW/8+C7W/lzt2aKkDBKEvL6JkFh/jQ2ESzBQ6GeAAAAJhvzH6gb8x+ | ||
oAAAAAtzc2gtZWQyNTUxOQAAACAPsAgW/8+C7W/lzt2aKkDBKEvL6JkFh/jQ2ESzBQ6GeA | ||
AAAECYlCpSV7VEkZ3BGK80K6LR64DVHl7qtrL8oVFJg4BrCw+wCBb/z4Ltb+XO3ZoqQMEo | ||
S8vomQWH+NDYRLMFDoZ4AAAAE3BhdHJpY2tAcGQtdGhpbmtwYWQBAg== | ||
-----END OPENSSH PRIVATE KEY----- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
package mockssh | ||
|
||
import ( | ||
_ "embed" | ||
"encoding/base64" | ||
"errors" | ||
"fmt" | ||
"log" | ||
"net" | ||
"strings" | ||
"sync" | ||
"testing" | ||
"time" | ||
|
||
"golang.org/x/crypto/ssh" | ||
) | ||
|
||
//go:embed host_key | ||
var hostKey []byte | ||
|
||
func newServerConfig(t *testing.T) *ssh.ServerConfig { | ||
return &ssh.ServerConfig{ | ||
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { | ||
if cert, ok := key.(*ssh.Certificate); ok { | ||
t.Log("SSH certificate received with key ID:", cert.KeyId) | ||
return &ssh.Permissions{CriticalOptions: cert.CriticalOptions, Extensions: cert.Extensions}, nil | ||
} | ||
return nil, fmt.Errorf("not accepting public key type: %s", key.Type()) | ||
}, | ||
AuthLogCallback: func(conn ssh.ConnMetadata, method string, err error) { | ||
if err == nil { | ||
t.Logf("SSH authenticated with user %s and method %s", conn.User(), method) | ||
} | ||
}, | ||
} | ||
} | ||
|
||
type MockSSHServer struct { | ||
t *testing.T | ||
validCommands map[string]string | ||
listener net.Listener | ||
port int | ||
hostKey ssh.Signer | ||
} | ||
|
||
// StartServer creates and starts a local SSH server. | ||
// The server will automatically be stopped when the test completes. | ||
func StartServer(t *testing.T, validCommands map[string]string) (*MockSSHServer, error) { | ||
hk, err := ssh.ParsePrivateKey(hostKey) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to parse host key: %v", err) | ||
} | ||
|
||
s := &MockSSHServer{ | ||
t: t, | ||
validCommands: validCommands, | ||
hostKey: hk, | ||
} | ||
if err := s.start(); err != nil { | ||
return nil, err | ||
} | ||
|
||
t.Cleanup(func() { | ||
if err := s.listener.Close(); err != nil { | ||
t.Fatal(err) | ||
} | ||
}) | ||
|
||
return s, nil | ||
} | ||
|
||
func (s *MockSSHServer) Port() int { | ||
return s.port | ||
} | ||
|
||
func (s *MockSSHServer) HostKeyConfig() string { | ||
return fmt.Sprintf("[127.0.0.1]:%d %s %s", | ||
s.port, | ||
s.hostKey.PublicKey().Type(), | ||
base64.StdEncoding.EncodeToString(s.hostKey.PublicKey().Marshal()), | ||
) | ||
} | ||
|
||
func (s *MockSSHServer) start() error { | ||
t := s.t | ||
|
||
config := newServerConfig(t) | ||
config.AddHostKey(s.hostKey) | ||
|
||
listener, err := net.Listen("tcp", "127.0.0.1:0") | ||
if err != nil { | ||
return err | ||
} | ||
s.listener = listener | ||
s.port = listener.Addr().(*net.TCPAddr).Port | ||
t.Logf("Test SSH server listening at %s", listener.Addr()) | ||
|
||
go func(l net.Listener, validCommands map[string]string) { | ||
for { | ||
conn, err := l.Accept() | ||
if err != nil { | ||
if errors.Is(err, net.ErrClosed) { | ||
break | ||
} | ||
t.Errorf("Failed to accept connection: %v", err) | ||
continue | ||
} | ||
|
||
sshConn, chans, reqs, err := ssh.NewServerConn(conn, config) | ||
if err != nil { | ||
log.Printf("Handshake failed: %v", err) | ||
return | ||
} | ||
|
||
log.Printf("New SSH connection from %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion()) | ||
|
||
wg := sync.WaitGroup{} | ||
wg.Add(1) | ||
go func() { | ||
ssh.DiscardRequests(reqs) | ||
wg.Done() | ||
}() | ||
wg.Add(1) | ||
go func() { | ||
s.handleChannels(chans) | ||
wg.Done() | ||
}() | ||
wg.Wait() | ||
} | ||
}(s.listener, s.validCommands) | ||
|
||
return nil | ||
} | ||
|
||
func (s *MockSSHServer) handleChannels(chans <-chan ssh.NewChannel) { | ||
t := s.t | ||
|
||
for newChannel := range chans { | ||
if newChannel.ChannelType() != "session" { | ||
err := newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") | ||
if err != nil { | ||
t.Errorf("Failed to reject channel: %v", err) | ||
} | ||
continue | ||
} | ||
|
||
channel, requests, err := newChannel.Accept() | ||
if err != nil { | ||
t.Errorf("Failed to accept channel: %v", err) | ||
return | ||
} | ||
|
||
timer := time.NewTimer(time.Second * 10) | ||
|
||
var exitWithStatus = make(chan uint32, 1) | ||
go func(in <-chan *ssh.Request) { | ||
for req := range in { | ||
if !req.WantReply { | ||
continue | ||
} | ||
switch req.Type { | ||
case "exec": | ||
err := req.Reply(true, nil) | ||
if err != nil { | ||
t.Errorf("Failed to reply to command: %v", err) | ||
} | ||
cmd := strings.TrimLeft(string(req.Payload), "\x00\x03") | ||
t.Logf("Command received: %s", cmd) | ||
if output := s.validCommands[cmd]; output != "" { | ||
_, err = channel.Write([]byte(output)) | ||
if err != nil { | ||
t.Errorf("Failed to write to channel: %v", err) | ||
} | ||
exitWithStatus <- 0 | ||
} else { | ||
_, _ = channel.Stderr().Write([]byte(fmt.Sprintf("Invalid command: %s", cmd))) | ||
exitWithStatus <- 1 | ||
} | ||
return | ||
default: | ||
_ = req.Reply(false, nil) | ||
} | ||
} | ||
}(requests) | ||
|
||
for { | ||
select { | ||
case s := <-exitWithStatus: | ||
_, err = channel.SendRequest("exit-status", false, ssh.Marshal(struct{ Status uint32 }{s})) | ||
if err != nil { | ||
t.Errorf("Failed to send exit status: %v", err) | ||
} | ||
goto close | ||
case <-timer.C: | ||
t.Error("Timed out") | ||
goto close | ||
} | ||
} | ||
|
||
close: | ||
_ = channel.Close() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
package tests | ||
|
||
import ( | ||
"net/http/httptest" | ||
"strconv" | ||
"testing" | ||
|
||
"github.com/platformsh/cli/pkg/mockapi" | ||
"github.com/platformsh/legacy-cli/tests/mockssh" | ||
) | ||
|
||
func TestSSH(t *testing.T) { | ||
authServer := mockapi.NewAuthServer(t) | ||
defer authServer.Close() | ||
|
||
myUserID := "my-user-id" | ||
|
||
sshServer, err := mockssh.StartServer(t, map[string]string{ | ||
"pwd": "/mock/path", | ||
}) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
projectID := "aiyaikii1uere" | ||
|
||
apiHandler := mockapi.NewHandler(t) | ||
apiHandler.SetMyUser(&mockapi.User{ID: myUserID}) | ||
apiHandler.SetProjects([]*mockapi.Project{ | ||
{ | ||
ID: projectID, | ||
Links: mockapi.MakeHALLinks( | ||
"self=/projects/"+projectID, | ||
"environments=/projects/"+projectID+"/environments", | ||
), | ||
DefaultBranch: "main", | ||
}, | ||
}) | ||
mainEnv := makeEnv(projectID, "main", "production", "active", nil) | ||
mainEnv.SetCurrentDeployment(&mockapi.Deployment{ | ||
WebApps: map[string]mockapi.App{ | ||
"app": {Name: "app", Type: "golang:1.23", Size: "M", Disk: 2048, Mounts: map[string]mockapi.Mount{}}, | ||
}, | ||
Services: map[string]mockapi.App{}, | ||
Workers: map[string]mockapi.Worker{}, | ||
Routes: mockRoutes(), | ||
Links: mockapi.MakeHALLinks("self=/projects/" + projectID + "/environments/main/deployment/current"), | ||
}) | ||
mainEnv.Links["pf:ssh:app:0"] = mockapi.HALLink{HREF: "ssh://[email protected]"} | ||
mainEnv.Links["pf:ssh:app:1"] = mockapi.HALLink{HREF: "ssh://[email protected]"} | ||
apiHandler.SetEnvironments([]*mockapi.Environment{ | ||
mainEnv, | ||
}) | ||
|
||
apiServer := httptest.NewServer(apiHandler) | ||
defer apiServer.Close() | ||
|
||
f := newCommandFactory(t, apiServer.URL, authServer.URL) | ||
f.extraEnv = []string{ | ||
EnvPrefix + "SSH_OPTIONS=HostName 127.0.0.1\nPort " + strconv.Itoa(sshServer.Port()), | ||
EnvPrefix + "SSH_HOST_KEYS=" + sshServer.HostKeyConfig(), | ||
} | ||
|
||
f.Run("cc") | ||
assertTrimmed(t, "/mock/path", f.Run("ssh", "-p", projectID, "-e", ".", "pwd")) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters