Skip to content

Commit b15019e

Browse files
authored
feat: Add SSH exec mode support (#42)
Handle exec requests (e.g., ssh host "show version") in addition to interactive shell sessions. Check s.RawCommand() at the top of the handler — if non-empty, process the command, write output, and exit. Abbreviated command matching works in exec mode. Closes #23 Co-authored-by: Brett Lykins <blykins@amazon.com>
1 parent 12ba87f commit b15019e

2 files changed

Lines changed: 114 additions & 1 deletion

File tree

ssh_server/handlers/ciscohandlers.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package handlers
44

55
import (
6+
"io"
67
"log"
78
"strings"
89

@@ -19,7 +20,23 @@ func GenericCiscoHandler(myFakeDevice *fakedevices.FakeDevice) {
1920
// Prepare the "ssh.DefaultHandler", this houses our device specific functionality
2021
ssh.Handle(func(s ssh.Session) {
2122

22-
// Setup our initial "context" or prompt
23+
// Exec mode: client sent a command directly (e.g., ssh host "show version")
24+
if cmd := s.RawCommand(); cmd != "" {
25+
log.Printf("exec: %s", cmd)
26+
match, matchedCommand, multipleMatches, _ := utils.CmdMatch(cmd, myFakeDevice.SupportedCommands)
27+
if match && !multipleMatches {
28+
output, err := fakedevices.TranscriptReader(
29+
myFakeDevice.SupportedCommands[matchedCommand], myFakeDevice,
30+
)
31+
if err == nil {
32+
io.WriteString(s, output)
33+
}
34+
}
35+
s.Exit(0)
36+
return
37+
}
38+
39+
// Interactive shell mode
2340
ContextState := myFakeDevice.ContextSearch["base"]
2441

2542
// Setup a terminal with the hostname + initial context state as a prompt

ssh_server/handlers/ciscohandlers_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,102 @@ func TestHandler_ResetState(t *testing.T) {
218218
}
219219
}
220220

221+
func TestExec_ShowVersion(t *testing.T) {
222+
fd := newTestDevice()
223+
addr, cleanup := startTestServer(t, fd)
224+
defer cleanup()
225+
226+
config := &gossh.ClientConfig{
227+
User: "admin",
228+
Auth: []gossh.AuthMethod{gossh.Password("admin")},
229+
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
230+
Timeout: 2 * time.Second,
231+
}
232+
client, err := gossh.Dial("tcp", addr, config)
233+
if err != nil {
234+
t.Fatalf("ssh dial: %v", err)
235+
}
236+
defer client.Close()
237+
238+
session, err := client.NewSession()
239+
if err != nil {
240+
t.Fatal(err)
241+
}
242+
defer session.Close()
243+
244+
out, err := session.Output("show version")
245+
if err != nil {
246+
t.Fatalf("exec: %v", err)
247+
}
248+
if !strings.Contains(string(out), "FakeOS version 1.0") {
249+
t.Errorf("expected 'FakeOS version 1.0' in exec output, got:\n%s", out)
250+
}
251+
}
252+
253+
func TestExec_AbbreviatedCommand(t *testing.T) {
254+
fd := newTestDevice()
255+
addr, cleanup := startTestServer(t, fd)
256+
defer cleanup()
257+
258+
config := &gossh.ClientConfig{
259+
User: "admin",
260+
Auth: []gossh.AuthMethod{gossh.Password("admin")},
261+
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
262+
Timeout: 2 * time.Second,
263+
}
264+
client, err := gossh.Dial("tcp", addr, config)
265+
if err != nil {
266+
t.Fatalf("ssh dial: %v", err)
267+
}
268+
defer client.Close()
269+
270+
session, err := client.NewSession()
271+
if err != nil {
272+
t.Fatal(err)
273+
}
274+
defer session.Close()
275+
276+
out, err := session.Output("sho ver")
277+
if err != nil {
278+
t.Fatalf("exec: %v", err)
279+
}
280+
if !strings.Contains(string(out), "FakeOS version 1.0") {
281+
t.Errorf("expected abbreviated match in exec output, got:\n%s", out)
282+
}
283+
}
284+
285+
func TestExec_UnknownCommand(t *testing.T) {
286+
fd := newTestDevice()
287+
addr, cleanup := startTestServer(t, fd)
288+
defer cleanup()
289+
290+
config := &gossh.ClientConfig{
291+
User: "admin",
292+
Auth: []gossh.AuthMethod{gossh.Password("admin")},
293+
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
294+
Timeout: 2 * time.Second,
295+
}
296+
client, err := gossh.Dial("tcp", addr, config)
297+
if err != nil {
298+
t.Fatalf("ssh dial: %v", err)
299+
}
300+
defer client.Close()
301+
302+
session, err := client.NewSession()
303+
if err != nil {
304+
t.Fatal(err)
305+
}
306+
defer session.Close()
307+
308+
out, err := session.Output("bogus command")
309+
if err != nil {
310+
t.Fatalf("exec: %v", err)
311+
}
312+
if len(out) != 0 {
313+
t.Errorf("expected empty output for unknown command, got:\n%s", out)
314+
}
315+
}
316+
221317
func TestHandler_EmptyInput(t *testing.T) {
222318
fd := newTestDevice()
223319
addr, cleanup := startTestServer(t, fd)

0 commit comments

Comments
 (0)