Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement remote-local http request forwarding #35

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 50 additions & 8 deletions cmd/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import (
"context"
"errors"
"fmt"
"net"
"os"
"os/signal"
"strconv"
"syscall"
"time"

"github.com/adelowo/sdump/config"
"github.com/adelowo/sdump/internal/forward"
"github.com/adelowo/sdump/internal/tui"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/log"
Expand All @@ -22,15 +25,28 @@ import (
)

func createSSHCommand(rootCmd *cobra.Command, cfg *config.Config) {
forwardHandler := forward.New()

cmd := &cobra.Command{
Use: "ssh",
Short: "Start/run the TUI app",
RunE: func(_ *cobra.Command, _ []string) error {
s, err := wish.NewServer(
wish.WithAddress(fmt.Sprintf("%s:%d", cfg.SSH.Host, cfg.SSH.Port)),
func(s *ssh.Server) error {
s.ReversePortForwardingCallback = func(_ ssh.Context, _ string, _ uint32) bool {
return true
}

s.RequestHandlers = map[string]ssh.RequestHandler{
"tcpip-forward": forwardHandler.HandleSSHRequest,
"cancel-tcpip-forward": forwardHandler.HandleSSHRequest,
}
return nil
},
validateSSHPublicKey(cfg),
wish.WithMiddleware(
bm.Middleware(teaHandler(cfg)),
bm.Middleware(teaHandler(cfg, forwardHandler)),
lm.Middleware(),
),
)
Expand Down Expand Up @@ -112,21 +128,47 @@ func validateSSHPublicKey(cfg *config.Config) ssh.Option {
})
}

func teaHandler(cfg *config.Config) func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
func teaHandler(cfg *config.Config,
fowardHandler *forward.Forwarder,
) func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
pty, _, active := s.Pty()
if !active {
wish.Fatalln(s, "no active terminal, skipping")
return nil, nil
// wish.Fatalln(s, "no active terminal, skipping")
// return nil, nil
}

sshFingerPrint := gossh.FingerprintSHA256(s.PublicKey())
var opts []tui.Option

tuiModel, err := tui.New(cfg,
tui.WithWidth(pty.Window.Width),
opts = append(opts, tui.WithWidth(pty.Window.Width),
tui.WithHeight(pty.Window.Height),
tui.WithSSHFingerPrint(sshFingerPrint),
tui.WithSSHFingerPrint(gossh.FingerprintSHA256(s.PublicKey())),
)

// NOTE: when we decide to expand this, please move the parsing of
// these commands out of here and simplify the logic
if len(s.Command()) == 2 {
if s.Command()[0] != "http" {
wish.Fatalln(s, "Only http commands supported")
return nil, nil
}

port, err := strconv.Atoi(s.Command()[1])
if err != nil {
wish.Fatal(s, "Please provide a valid port number to forward http requests to")
return nil, nil
}

host, _, err := net.SplitHostPort(s.RemoteAddr().String())
if err != nil {
wish.Fatalln(s, "could not fetch your remote address for port forwarding")
return nil, nil
}

opts = append(opts, tui.WithHTTPForwarding(true, host, port))
}

tuiModel, err := tui.New(cfg, fowardHandler, opts...)
if err != nil {
wish.Fatalln(s, fmt.Errorf("%v...Could not set up TUI session", err))
return nil, nil
Expand Down
45 changes: 45 additions & 0 deletions internal/forward/forward.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package forward

import (
"sync"

"github.com/charmbracelet/ssh"
gossh "golang.org/x/crypto/ssh"
)

type ConnectionInfo struct {
Port int
Host string

// do we even need this really?
// since we already have the host and port
// RemoteAddr net.Addr
}

type Forwarder struct {
// known connections to the ssh server.
// the key here is the host of the connecting user
connections map[string]ConnectionInfo
mutex sync.RWMutex
}

func New() *Forwarder {
return &Forwarder{
mutex: sync.RWMutex{},
connections: make(map[string]ConnectionInfo),
}
}

func (f *Forwarder) AddConnection(key string, data ConnectionInfo) {
f.mutex.Lock()
defer f.mutex.Unlock()

f.connections[key] = data
}

func (f *Forwarder) HandleSSHRequest(ctx ssh.Context,
srv *ssh.Server,
req *gossh.Request,
) (bool, []byte) {
return true, nil
}
131 changes: 81 additions & 50 deletions internal/tui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"time"

"github.com/adelowo/sdump/config"
"github.com/adelowo/sdump/internal/forward"
"github.com/adelowo/sdump/internal/util"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/spinner"
Expand Down Expand Up @@ -47,9 +48,15 @@ type model struct {
width, height int

sshFingerPrint string

host string
forwarder *forward.Forwarder
isHTTPForwardingEnabled bool
portToForwardTo int
}

func New(cfg *config.Config,
forwarder *forward.Forwarder,
opts ...Option,
) (tea.Model, error) {
width, height, err := term.GetSize(int(os.Stderr.Fd()))
Expand All @@ -63,6 +70,22 @@ func New(cfg *config.Config,
opt(&tuiModel)
}

if tuiModel.isHTTPForwardingEnabled {
if tuiModel.portToForwardTo <= 0 {
return nil, errors.New("please provide a non zero or negative port to forward your requests to")
}

if util.IsStringEmpty(tuiModel.host) {
return nil, errors.New("please provide a valid host address")
}

if forwarder == nil {
return nil, errors.New("could not set up http forwarding")
}

tuiModel.forwarder = forwarder
}

if util.IsStringEmpty(tuiModel.sshFingerPrint) {
return nil, errors.New("SSH fingerprint must be provided")
}
Expand All @@ -74,56 +97,6 @@ func New(cfg *config.Config,
return tuiModel, nil
}

func newModel(cfg *config.Config, width, height int) model {
columns := []table.Column{
{
Title: "Header",
Width: 50,
},
{
Title: "Value",
Width: 50,
},
}

m := model{
width: width,
height: height,
title: "Sdump",
spinner: spinner.New(
spinner.WithSpinner(spinner.Line),
spinner.WithStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("205"))),
),

cfg: cfg,
httpClient: &http.Client{
Timeout: time.Minute,
},

requestList: list.New([]list.Item{}, list.NewDefaultDelegate(), 50, height),
detailedRequestView: viewport.New(width, height),
detailedRequestViewBuffer: bytes.NewBuffer(nil),
sseClient: sse.NewClient(fmt.Sprintf("%s/events", cfg.HTTP.Domain)),
receiveChan: make(chan item),

headersTable: table.New(table.WithColumns(columns),
table.WithFocused(true),
table.WithHeight(10),
table.WithWidth(width),
table.WithKeyMap(table.KeyMap{}),
table.WithStyles(getTableStyles())),
}

m.requestList.Title = "Incoming requests"
m.requestList.SetShowTitle(true)
m.requestList.SetFilteringEnabled(false)
m.requestList.DisableQuitKeybindings()

m.headersTable.Blur()

return m
}

func (m model) isInitialized() bool { return m.dumpURL != nil }

func (m model) Init() tea.Cmd {
Expand Down Expand Up @@ -191,6 +164,7 @@ func (m model) createEndpoint(forceURLChange bool) func() tea.Msg {
var response struct {
URL struct {
HumanReadableEndpoint string `json:"human_readable_endpoint,omitempty"`
Identifier string `json:"identifier"`
} `json:"url,omitempty"`
SSE struct {
Channel string `json:"channel,omitempty"`
Expand All @@ -201,6 +175,13 @@ func (m model) createEndpoint(forceURLChange bool) func() tea.Msg {
return ErrorMsg{err: err}
}

if m.isHTTPForwardingEnabled {
m.forwarder.AddConnection(response.URL.Identifier, forward.ConnectionInfo{
Port: m.portToForwardTo,
Host: m.host,
})
}

return DumpURLMsg{
URL: response.URL.HumanReadableEndpoint,
SSEChannel: response.SSE.Channel,
Expand Down Expand Up @@ -381,3 +362,53 @@ func (m model) makeTable() string {

return m.buildView()
}

func newModel(cfg *config.Config, width, height int) model {
columns := []table.Column{
{
Title: "Header",
Width: 50,
},
{
Title: "Value",
Width: 50,
},
}

m := model{
width: width,
height: height,
title: "Sdump",
spinner: spinner.New(
spinner.WithSpinner(spinner.Line),
spinner.WithStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("205"))),
),

cfg: cfg,
httpClient: &http.Client{
Timeout: time.Minute,
},

requestList: list.New([]list.Item{}, list.NewDefaultDelegate(), 50, height),
detailedRequestView: viewport.New(width, height),
detailedRequestViewBuffer: bytes.NewBuffer(nil),
sseClient: sse.NewClient(fmt.Sprintf("%s/events", cfg.HTTP.Domain)),
receiveChan: make(chan item),

headersTable: table.New(table.WithColumns(columns),
table.WithFocused(true),
table.WithHeight(10),
table.WithWidth(width),
table.WithKeyMap(table.KeyMap{}),
table.WithStyles(getTableStyles())),
}

m.requestList.Title = "Incoming requests"
m.requestList.SetShowTitle(true)
m.requestList.SetFilteringEnabled(false)
m.requestList.DisableQuitKeybindings()

m.headersTable.Blur()

return m
}
14 changes: 13 additions & 1 deletion internal/tui/option.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
package tui

import "github.com/adelowo/sdump/config"
import (
"github.com/adelowo/sdump/config"
)

type Option func(*model)

func WithHTTPForwarding(forwardRequests bool,
host string, portToForwardTo int,
) Option {
return func(m *model) {
m.isHTTPForwardingEnabled = forwardRequests
m.portToForwardTo = portToForwardTo
m.host = host
}
}

func WithConfig(cfg *config.Config) Option {
return func(m *model) {
m.cfg = cfg
Expand Down
Loading