commit
d829810b8b
13 changed files with 468 additions and 0 deletions
@ -0,0 +1,14 @@
@@ -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 @@
@@ -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,5 @@
@@ -0,0 +1,5 @@
|
||||
.idea/ |
||||
dist/ |
||||
vendor/ |
||||
sshtargate |
||||
*.sh |
@ -0,0 +1,33 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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