forked from charmbracelet/wishlist
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: proxy connections (charmbracelet#1)
* wip Signed-off-by: Carlos A Becker <[email protected]> * wip Signed-off-by: Carlos A Becker <[email protected]> * wip Signed-off-by: Carlos A Becker <[email protected]> * wip: proxy Signed-off-by: Carlos A Becker <[email protected]> * fix: res Signed-off-by: Carlos A Becker <[email protected]> * fix: stdin leak Signed-off-by: Carlos A Becker <[email protected]> * feat: improvements Signed-off-by: Carlos A Becker <[email protected]> * fix: improve example Signed-off-by: Carlos A Becker <[email protected]> * fix: connect on the cmd instead Signed-off-by: Carlos A Becker <[email protected]> * fix: notify win changes Signed-off-by: Carlos A Becker <[email protected]> * chore: improv example Signed-off-by: Carlos A Becker <[email protected]> * refactor: noop model Signed-off-by: Carlos A Becker <[email protected]> * refactor: improve client Signed-off-by: Carlos A Becker <[email protected]> * fix: fail on used addr Signed-off-by: Carlos A Becker <[email protected]> * refactor: move code around Signed-off-by: Carlos A Becker <[email protected]> * wip Signed-off-by: Carlos A Becker <[email protected]> * refactor: improve code a bit * simplify Signed-off-by: Carlos A Becker <[email protected]> * fix: use keygen Signed-off-by: Carlos A Becker <[email protected]> * chore: organizing Signed-off-by: Carlos A Becker <[email protected]> * feat: allow to ssh -t directly into an app Signed-off-by: Carlos A Becker <[email protected]> * fix: log Signed-off-by: Carlos A Becker <[email protected]> * feat: agent forwarding Signed-off-by: Carlos A Becker <[email protected]> * docs: improve Signed-off-by: Carlos A Becker <[email protected]> * wip Signed-off-by: Carlos A Becker <[email protected]> * feat: allow to use custom users Signed-off-by: Carlos A Becker <[email protected]> * wip Signed-off-by: Carlos A Becker <[email protected]> * feat: handoff Signed-off-by: Carlos A Becker <[email protected]>
- Loading branch information
Showing
11 changed files
with
503 additions
and
183 deletions.
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
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
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,190 @@ | ||
package wishlist | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"log" | ||
"net" | ||
|
||
"github.com/charmbracelet/keygen" | ||
"github.com/gliderlabs/ssh" | ||
"github.com/muesli/termenv" | ||
gossh "golang.org/x/crypto/ssh" | ||
"golang.org/x/crypto/ssh/agent" | ||
) | ||
|
||
func resetPty(w io.Writer) { | ||
fmt.Fprint(w, termenv.CSI+termenv.ExitAltScreenSeq) | ||
fmt.Fprint(w, termenv.CSI+termenv.ResetSeq+"m") | ||
} | ||
|
||
func MustConnect(s ssh.Session, e *Endpoint) { | ||
if err := connect(s, e); err != nil { | ||
fmt.Fprintln(s, err.Error()) | ||
s.Exit(1) | ||
return //unreachable | ||
} | ||
fmt.Fprintf(s, "Closed connection to %q (%s)\n", e.Name, e.Address) | ||
s.Exit(0) | ||
} | ||
|
||
func connect(prev ssh.Session, e *Endpoint) error { | ||
resetPty(prev) | ||
defer resetPty(prev) | ||
|
||
methods, closers, err := authMethods(prev) | ||
defer closers.close() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
conf := &gossh.ClientConfig{ | ||
User: firstNonEmpty(e.User, prev.User()), | ||
HostKeyCallback: gossh.InsecureIgnoreHostKey(), | ||
Auth: methods, | ||
} | ||
|
||
conn, err := gossh.Dial("tcp", e.Address, conf) | ||
if err != nil { | ||
return err | ||
} | ||
defer func() { | ||
if err := conn.Close(); err != nil { | ||
log.Println("failed to close conn:", err) | ||
} | ||
}() | ||
|
||
session, err := conn.NewSession() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
defer func() { | ||
if err := session.Close(); err != nil { | ||
log.Println("failed to close session:", err) | ||
} | ||
}() | ||
|
||
session.Stdout = prev | ||
session.Stderr = prev.Stderr() | ||
session.Stdin = prev | ||
|
||
pty, winch, _ := prev.Pty() | ||
w := pty.Window | ||
if err := session.RequestPty(pty.Term, w.Height, w.Width, nil); err != nil { | ||
return err | ||
} | ||
|
||
done := make(chan bool, 1) | ||
defer func() { done <- true }() | ||
|
||
go notifyWindowChanges(session, done, winch) | ||
|
||
// Non blocking: | ||
// - session.Shell() | ||
// - session.Start() | ||
// | ||
// Blocking: | ||
// - session.Run() | ||
// - session.Output() | ||
// - session.CombinedOutput() | ||
// - session.Wait() | ||
// | ||
if err := session.Shell(); err != nil { | ||
return err | ||
} | ||
|
||
return session.Wait() | ||
} | ||
|
||
func notifyWindowChanges(session *gossh.Session, done <-chan bool, winch <-chan ssh.Window) { | ||
for { | ||
select { | ||
case <-done: | ||
log.Println("winch done") | ||
return | ||
case w := <-winch: | ||
if w.Height == 0 && w.Width == 0 { | ||
// this only happens if the session is already dead, make sure there are no leftovers | ||
return | ||
} | ||
if err := session.WindowChange(w.Height, w.Width); err != nil { | ||
log.Println("failed to notify window change", err) | ||
return | ||
} | ||
} | ||
} | ||
} | ||
|
||
type closers []func() error | ||
|
||
func (c closers) close() { | ||
for _, closer := range c { | ||
if err := closer(); err != nil { | ||
log.Println("failed to close:", err) | ||
} | ||
} | ||
} | ||
|
||
func authMethods(s ssh.Session) ([]gossh.AuthMethod, closers, error) { | ||
var authMethods []gossh.AuthMethod | ||
methods, closers, err := tryAuthAgent(s) | ||
if err != nil { | ||
return methods, closers, err | ||
} | ||
if methods != nil { | ||
authMethods = append(authMethods, methods...) | ||
} | ||
|
||
methods, err = tryNewKey() | ||
if err != nil { | ||
return methods, closers, err | ||
} | ||
return append(authMethods, methods...), closers, nil | ||
} | ||
|
||
func tryAuthAgent(s ssh.Session) ([]gossh.AuthMethod, closers, error) { | ||
ok, err := s.SendRequest("[email protected]", true, nil) | ||
log.Println("agent forward:", ok, err, ssh.AgentRequested(s)) | ||
|
||
if ssh.AgentRequested(s) { | ||
l, err := ssh.NewAgentListener() | ||
if err != nil { | ||
return nil, nil, err | ||
} | ||
go ssh.ForwardAgentConnections(l, s) | ||
|
||
conn, err := net.Dial(l.Addr().Network(), l.Addr().String()) | ||
if err != nil { | ||
return nil, closers{l.Close}, err | ||
} | ||
|
||
return []gossh.AuthMethod{ | ||
gossh.PublicKeysCallback(agent.NewClient(conn).Signers), | ||
}, closers{l.Close, conn.Close}, nil | ||
} | ||
|
||
return nil, nil, nil | ||
} | ||
|
||
func tryNewKey() ([]gossh.AuthMethod, error) { | ||
key, err := keygen.New("", "", nil, keygen.Ed25519) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
signer, err := gossh.ParsePrivateKey(key.PrivateKeyPEM) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return []gossh.AuthMethod{gossh.PublicKeys(signer)}, nil | ||
} | ||
|
||
func firstNonEmpty(ss ...string) string { | ||
for _, s := range ss { | ||
if s != "" { | ||
return s | ||
} | ||
} | ||
return "" | ||
} |
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
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,37 @@ | ||
package wishlist | ||
|
||
import ( | ||
"github.com/charmbracelet/wish" | ||
"github.com/gliderlabs/ssh" | ||
) | ||
|
||
const HandoffContextKey = "handoff-to" | ||
|
||
// Endpoint represents an endpoint to list. | ||
// If it has a Handler, wishlist will start an SSH server on the given address. | ||
type Endpoint struct { | ||
Name string `yaml:"name"` | ||
Address string `yaml:"address"` | ||
User string `yaml:"user"` | ||
Middlewares []wish.Middleware `yaml:"-"` | ||
} | ||
|
||
// Returns true if the endpoint is valid. | ||
func (e Endpoint) Valid() bool { | ||
return e.Name != "" && (len(e.Middlewares) > 0 || e.Address != "") | ||
} | ||
|
||
// ShouldListen returns true if we should start a server for this endpoint. | ||
func (e Endpoint) ShouldListen() bool { | ||
return len(e.Middlewares) > 0 | ||
} | ||
|
||
// Config represents the wishlist configuration. | ||
type Config struct { | ||
Listen string `yaml:"listen"` | ||
Port int64 `yaml:"port"` | ||
Endpoints []*Endpoint `yaml:"endpoints"` | ||
Factory func(Endpoint) (*ssh.Server, error) `yaml:"-"` | ||
|
||
lastPort int64 | ||
} |
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,2 @@ | ||
// Package wishlist provides a library and binary to list and connect to SSH apps. | ||
package wishlist |
Oops, something went wrong.