commit
d829810b8b
13 changed files with 468 additions and 0 deletions
@ -0,0 +1,14 @@ |
|||||||
|
arch: amd64 |
||||||
|
environment: |
||||||
|
PROJECT_NAME: 'sshtargate' |
||||||
|
CGO_ENABLED: '1' |
||||||
|
GO111MODULE: 'on' |
||||||
|
image: freebsd/latest |
||||||
|
packages: |
||||||
|
- go |
||||||
|
sources: |
||||||
|
- https://git.sr.ht/~tslocum/sshtargate |
||||||
|
tasks: |
||||||
|
- test: | |
||||||
|
cd $PROJECT_NAME |
||||||
|
go test ./... |
@ -0,0 +1,14 @@ |
|||||||
|
arch: x86_64 |
||||||
|
environment: |
||||||
|
PROJECT_NAME: 'sshtargate' |
||||||
|
CGO_ENABLED: '1' |
||||||
|
GO111MODULE: 'on' |
||||||
|
image: alpine/edge |
||||||
|
packages: |
||||||
|
- go |
||||||
|
sources: |
||||||
|
- https://git.sr.ht/~tslocum/sshtargate |
||||||
|
tasks: |
||||||
|
- test: | |
||||||
|
cd $PROJECT_NAME |
||||||
|
go test ./... |
@ -0,0 +1,33 @@ |
|||||||
|
This document explains how to configure sshtargate. |
||||||
|
|
||||||
|
# Configuration |
||||||
|
|
||||||
|
Specify the path to a configuration file with ```--config``` or create a file |
||||||
|
at the default path of ```~/.config/sshtargate/config.yaml```. |
||||||
|
|
||||||
|
Define one or more **portals** by name/label with the following options: |
||||||
|
|
||||||
|
- **command** - Command to execute |
||||||
|
- **host** - One or more addresses to listen for connections |
||||||
|
|
||||||
|
# Example config.yaml |
||||||
|
|
||||||
|
``` |
||||||
|
portals: |
||||||
|
date and time: |
||||||
|
command: date |
||||||
|
host: |
||||||
|
- localhost:19001 |
||||||
|
uname: |
||||||
|
command: uname -a |
||||||
|
host: |
||||||
|
- localhost:19002 |
||||||
|
process list: |
||||||
|
command: ps -aux |
||||||
|
host: |
||||||
|
- localhost:19003 |
||||||
|
system monitor: |
||||||
|
command: htop |
||||||
|
host: |
||||||
|
- localhost:19004 |
||||||
|
``` |
@ -0,0 +1,21 @@ |
|||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2020 Trevor Slocum <trevor@rocketnine.space> |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
@ -0,0 +1,28 @@ |
|||||||
|
# sshtargate |
||||||
|
[](https://godoc.org/git.sr.ht/~tslocum/sshtargate) |
||||||
|
[](https://builds.sr.ht/~tslocum/sshtargate) |
||||||
|
[](https://liberapay.com/rocketnine.space) |
||||||
|
|
||||||
|
Host SSH portals to applications |
||||||
|
|
||||||
|
## Install |
||||||
|
|
||||||
|
Choose one of the following methods: |
||||||
|
|
||||||
|
### Download |
||||||
|
|
||||||
|
[**Download sshtargate**](https://sshtargate.rocketnine.space/download/?sort=name&order=desc) |
||||||
|
|
||||||
|
### Compile |
||||||
|
|
||||||
|
``` |
||||||
|
GO111MODULE=on go get git.sr.ht/~tslocum/sshtargate |
||||||
|
``` |
||||||
|
|
||||||
|
## Configure |
||||||
|
|
||||||
|
See [CONFIGURATION.md](https://man.sr.ht/~tslocum/sshtargate/CONFIGURATION.md) |
||||||
|
|
||||||
|
## Support |
||||||
|
|
||||||
|
Please share issues/suggestions [here](https://todo.sr.ht/~tslocum/sshtargate). |
@ -0,0 +1,38 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"io/ioutil" |
||||||
|
"log" |
||||||
|
|
||||||
|
"gopkg.in/yaml.v2" |
||||||
|
) |
||||||
|
|
||||||
|
type PortalConfig struct { |
||||||
|
Command string |
||||||
|
Host []string `yaml:",flow"` |
||||||
|
} |
||||||
|
|
||||||
|
type Config struct { |
||||||
|
Portals map[string]*PortalConfig |
||||||
|
} |
||||||
|
|
||||||
|
var config = &Config{} |
||||||
|
|
||||||
|
func readConfig(configPath string) error { |
||||||
|
configData, err := ioutil.ReadFile(configPath) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("failed to read file: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
err = yaml.Unmarshal(configData, config) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("failed to parse file: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
if len(config.Portals) == 0 { |
||||||
|
log.Println("Warning: No portals are defined") |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
module git.sr.ht/~tslocum/sshtargate |
||||||
|
|
||||||
|
go 1.13 |
||||||
|
|
||||||
|
require ( |
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 |
||||||
|
github.com/creack/pty v1.1.9 |
||||||
|
github.com/gliderlabs/ssh v0.2.2 |
||||||
|
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 |
||||||
|
golang.org/x/sys v0.0.0-20200103143344-a1369afcdac7 // indirect |
||||||
|
gopkg.in/yaml.v2 v2.2.7 |
||||||
|
) |
@ -0,0 +1,20 @@ |
|||||||
|
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= |
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= |
||||||
|
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= |
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= |
||||||
|
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= |
||||||
|
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= |
||||||
|
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= |
||||||
|
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= |
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
||||||
|
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 h1:sKJQZMuxjOAR/Uo2LBfU90onWEf1dF4C+0hPJCc9Mpc= |
||||||
|
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= |
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||||
|
golang.org/x/sys v0.0.0-20200103143344-a1369afcdac7 h1:/W9OPMnnpmFXHYkcp2rQsbFUbRlRzfECQjmAFiOyHE8= |
||||||
|
golang.org/x/sys v0.0.0-20200103143344-a1369afcdac7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||||
|
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= |
||||||
|
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
@ -0,0 +1,33 @@ |
|||||||
|
project_name: sshtargate |
||||||
|
|
||||||
|
builds: |
||||||
|
- |
||||||
|
id: sshtargate |
||||||
|
binary: sshtargate |
||||||
|
env: |
||||||
|
- CGO_ENABLED=0 |
||||||
|
ldflags: |
||||||
|
- -s -w -X git.sr.ht/~tslocum/sshtargate/version={{.Version}} |
||||||
|
goos: |
||||||
|
- darwin |
||||||
|
- freebsd |
||||||
|
- linux |
||||||
|
goarch: |
||||||
|
- 386 |
||||||
|
- amd64 |
||||||
|
archives: |
||||||
|
- |
||||||
|
id: sshtargate |
||||||
|
builds: |
||||||
|
- sshtargate |
||||||
|
replacements: |
||||||
|
386: i386 |
||||||
|
format_overrides: |
||||||
|
- goos: windows |
||||||
|
format: zip |
||||||
|
files: |
||||||
|
- ./*.md |
||||||
|
- CHANGELOG |
||||||
|
- LICENSE |
||||||
|
checksum: |
||||||
|
name_template: 'checksums.txt' |
@ -0,0 +1,115 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"flag" |
||||||
|
"fmt" |
||||||
|
"log" |
||||||
|
"os" |
||||||
|
"os/signal" |
||||||
|
"path" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
"syscall" |
||||||
|
|
||||||
|
"git.sr.ht/~tslocum/sshtargate/pkg/gate" |
||||||
|
"github.com/anmitsu/go-shlex" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
version = "0.0.0" |
||||||
|
|
||||||
|
versionInfo = `sshtargate - Host SSH portals to applications - v` + version + ` |
||||||
|
https://git.sr.ht/~tslocum/sshtargate
|
||||||
|
The MIT License (MIT) |
||||||
|
Copyright (c) 2020 Trevor Slocum <trevor@rocketnine.space> |
||||||
|
` |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
printVersionInfo bool |
||||||
|
configPath string |
||||||
|
|
||||||
|
portals []*gate.Portal |
||||||
|
portalsLock = new(sync.Mutex) |
||||||
|
|
||||||
|
done = make(chan bool) |
||||||
|
) |
||||||
|
|
||||||
|
func main() { |
||||||
|
flag.BoolVar(&printVersionInfo, "version", false, "print version information and exit") |
||||||
|
flag.StringVar(&configPath, "config", "", "path to configuration file") |
||||||
|
flag.Parse() |
||||||
|
|
||||||
|
if printVersionInfo { |
||||||
|
fmt.Print(versionInfo) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: Allow portals to be specified via arguments
|
||||||
|
|
||||||
|
// TODO: Catch SIGHUP
|
||||||
|
sigc := make(chan os.Signal, 1) |
||||||
|
signal.Notify(sigc, |
||||||
|
syscall.SIGINT, |
||||||
|
syscall.SIGTERM) |
||||||
|
go func() { |
||||||
|
<-sigc |
||||||
|
|
||||||
|
done <- true |
||||||
|
}() |
||||||
|
|
||||||
|
log.Println("Initializing sshtargate...") |
||||||
|
|
||||||
|
if configPath == "" { |
||||||
|
homedir, err := os.UserHomeDir() |
||||||
|
if err == nil && homedir != "" { |
||||||
|
configPath = path.Join(homedir, ".config", "sshtargate", "config.yaml") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
err := readConfig(configPath) |
||||||
|
if err != nil { |
||||||
|
log.Fatalf("failed to read configuration file %s: %s", configPath, err) |
||||||
|
} |
||||||
|
|
||||||
|
for pname, pcfg := range config.Portals { |
||||||
|
cs, err := shlex.Split(pcfg.Command, true) |
||||||
|
if err != nil { |
||||||
|
log.Fatalf("failed to split command %s", pcfg.Command) |
||||||
|
} |
||||||
|
|
||||||
|
pname, pcfg := pname, pcfg // Capture
|
||||||
|
go func() { |
||||||
|
wg := new(sync.WaitGroup) |
||||||
|
|
||||||
|
for _, address := range pcfg.Host { |
||||||
|
wg.Add(1) |
||||||
|
address := address // Capture
|
||||||
|
|
||||||
|
go func() { |
||||||
|
p, err := gate.NewPortal(pname, address, cs) |
||||||
|
if err != nil { |
||||||
|
log.Fatalf("failed to start portal %s on %s: %s", pname, address, err) |
||||||
|
} |
||||||
|
|
||||||
|
portalsLock.Lock() |
||||||
|
portals = append(portals, p) |
||||||
|
portalsLock.Unlock() |
||||||
|
|
||||||
|
wg.Done() |
||||||
|
}() |
||||||
|
} |
||||||
|
|
||||||
|
wg.Wait() |
||||||
|
log.Printf("Opened portal %s on %s to %s", pname, strings.Join(pcfg.Host, ","), pcfg.Command) |
||||||
|
}() |
||||||
|
} |
||||||
|
|
||||||
|
<-done |
||||||
|
|
||||||
|
portalsLock.Lock() |
||||||
|
for _, p := range portals { |
||||||
|
p.Shutdown() |
||||||
|
} |
||||||
|
portalsLock.Unlock() |
||||||
|
} |
@ -0,0 +1,133 @@ |
|||||||
|
package gate |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"os" |
||||||
|
"os/exec" |
||||||
|
"path" |
||||||
|
"syscall" |
||||||
|
"time" |
||||||
|
"unsafe" |
||||||
|
|
||||||
|
"github.com/creack/pty" |
||||||
|
"github.com/gliderlabs/ssh" |
||||||
|
gossh "golang.org/x/crypto/ssh" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
ListenTimeout = 1 * time.Second |
||||||
|
IdleTimeout = 1 * time.Minute |
||||||
|
) |
||||||
|
|
||||||
|
type Portal struct { |
||||||
|
Name string |
||||||
|
Address string |
||||||
|
Command []string |
||||||
|
Server *ssh.Server |
||||||
|
} |
||||||
|
|
||||||
|
func setWinsize(f *os.File, w, h int) { |
||||||
|
syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ), |
||||||
|
uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0}))) |
||||||
|
} |
||||||
|
|
||||||
|
func NewPortal(name string, address string, command []string) (*Portal, error) { |
||||||
|
if address == "" { |
||||||
|
return nil, errors.New("no address supplied") |
||||||
|
} else if command == nil || command[0] == "" { |
||||||
|
return nil, errors.New("no command supplied") |
||||||
|
} |
||||||
|
|
||||||
|
server := &ssh.Server{ |
||||||
|
Addr: address, |
||||||
|
IdleTimeout: IdleTimeout, |
||||||
|
Handler: func(sshSession ssh.Session) { |
||||||
|
ptyReq, winCh, isPty := sshSession.Pty() |
||||||
|
if !isPty { |
||||||
|
io.WriteString(sshSession, "failed to start command: non-interactive terminals are not supported\n") |
||||||
|
sshSession.Exit(1) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
cmdCtx, cancelCmd := context.WithCancel(sshSession.Context()) |
||||||
|
defer cancelCmd() |
||||||
|
|
||||||
|
var args []string |
||||||
|
if len(command) > 1 { |
||||||
|
args = command[1:] |
||||||
|
} |
||||||
|
|
||||||
|
cmd := exec.CommandContext(cmdCtx, command[0], args...) |
||||||
|
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term)) |
||||||
|
|
||||||
|
f, err := pty.Start(cmd) |
||||||
|
if err != nil { |
||||||
|
io.WriteString(sshSession, fmt.Sprintf("failed to start command: failed to initialize pseudo-terminal: %s\n", err)) |
||||||
|
sshSession.Exit(1) |
||||||
|
return |
||||||
|
} |
||||||
|
defer f.Close() |
||||||
|
|
||||||
|
go func() { |
||||||
|
for win := range winCh { |
||||||
|
setWinsize(f, win.Width, win.Height) |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
go func() { |
||||||
|
io.Copy(f, sshSession) |
||||||
|
}() |
||||||
|
io.Copy(sshSession, f) |
||||||
|
|
||||||
|
cmd.Wait() |
||||||
|
}, |
||||||
|
PtyCallback: func(ctx ssh.Context, pty ssh.Pty) bool { |
||||||
|
return true |
||||||
|
}, |
||||||
|
PublicKeyHandler: func(ctx ssh.Context, key ssh.PublicKey) bool { |
||||||
|
return true |
||||||
|
}, |
||||||
|
PasswordHandler: func(ctx ssh.Context, password string) bool { |
||||||
|
return true |
||||||
|
}, |
||||||
|
KeyboardInteractiveHandler: func(ctx ssh.Context, challenger gossh.KeyboardInteractiveChallenge) bool { |
||||||
|
return true |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
homeDir, err := os.UserHomeDir() |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("failed to retrieve user home dir: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
err = server.SetOption(ssh.HostKeyFile(path.Join(homeDir, ".ssh", "id_rsa"))) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("failed to set host key file: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
t := time.NewTimer(ListenTimeout) |
||||||
|
errs := make(chan error) |
||||||
|
go func() { |
||||||
|
err := server.ListenAndServe() |
||||||
|
if err != nil { |
||||||
|
errs <- fmt.Errorf("failed to start SSH server: %s", err) |
||||||
|
} |
||||||
|
}() |
||||||
|
select { |
||||||
|
case err = <-errs: |
||||||
|
return nil, err |
||||||
|
case <-t.C: |
||||||
|
// Server started
|
||||||
|
} |
||||||
|
|
||||||
|
p := Portal{Name: name, Address: address, Command: command, Server: server} |
||||||
|
|
||||||
|
return &p, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (p *Portal) Shutdown() { |
||||||
|
p.Server.Close() |
||||||
|
} |
Loading…
Reference in new issue