Initial commit
This commit is contained in:
commit
d829810b8b
13 changed files with 468 additions and 0 deletions
14
.builds/amd64_freebsd.yml
Normal file
14
.builds/amd64_freebsd.yml
Normal file
|
@ -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 ./...
|
14
.builds/amd64_linux_alpine.yml
Normal file
14
.builds/amd64_linux_alpine.yml
Normal file
|
@ -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 ./...
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
.idea/
|
||||
dist/
|
||||
vendor/
|
||||
sshtargate
|
||||
*.sh
|
2
CHANGELOG
Normal file
2
CHANGELOG
Normal file
|
@ -0,0 +1,2 @@
|
|||
0.1.0:
|
||||
- Initial release
|
33
CONFIGURATION.md
Normal file
33
CONFIGURATION.md
Normal file
|
@ -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
|
||||
```
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -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.
|
28
README.md
Normal file
28
README.md
Normal file
|
@ -0,0 +1,28 @@
|
|||
# sshtargate
|
||||
[![GoDoc](https://godoc.org/git.sr.ht/~tslocum/sshtargate?status.svg)](https://godoc.org/git.sr.ht/~tslocum/sshtargate)
|
||||
[![builds.sr.ht status](https://builds.sr.ht/~tslocum/sshtargate.svg)](https://builds.sr.ht/~tslocum/sshtargate)
|
||||
[![Donate](https://img.shields.io/liberapay/receives/rocketnine.space.svg?logo=liberapay)](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).
|
38
config.go
Normal file
38
config.go
Normal file
|
@ -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
|
||||
}
|
12
go.mod
Normal file
12
go.mod
Normal file
|
@ -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
|
||||
)
|
20
go.sum
Normal file
20
go.sum
Normal file
|
@ -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=
|
33
goreleaser.yml
Normal file
33
goreleaser.yml
Normal file
|
@ -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'
|
115
main.go
Normal file
115
main.go
Normal file
|
@ -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()
|
||||
}
|
133
pkg/gate/portal.go
Normal file
133
pkg/gate/portal.go
Normal file
|
@ -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 a new issue