Revert "AUTH-1941: Adds initial SSH server implementation"
This reverts commit e9c9bf3cbd
.
This commit is contained in:
parent
026f2d1cc9
commit
47254113ee
|
@ -25,14 +25,6 @@
|
||||||
revision = "3f9954f6f6697845b082ca57995849ddf614f450"
|
revision = "3f9954f6f6697845b082ca57995849ddf614f450"
|
||||||
version = "v1.3.3"
|
version = "v1.3.3"
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:1a200e7e73293b75eb8e5c93d023b5472663432da0b663e1532624fcfede9ca8"
|
|
||||||
name = "github.com/anmitsu/go-shlex"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = "UT"
|
|
||||||
revision = "648efa622239a2f6ff949fed78ee37b48d499ba4"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:d6afaeed1502aa28e80a4ed0981d570ad91b2579193404256ce672ed0a609e0d"
|
digest = "1:d6afaeed1502aa28e80a4ed0981d570ad91b2579193404256ce672ed0a609e0d"
|
||||||
name = "github.com/beorn7/perks"
|
name = "github.com/beorn7/perks"
|
||||||
|
@ -120,14 +112,6 @@
|
||||||
revision = "95778dfbb74eb7e4dbaf43bf7d71809650ef8076"
|
revision = "95778dfbb74eb7e4dbaf43bf7d71809650ef8076"
|
||||||
version = "v19"
|
version = "v19"
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:385dff5bf83bf1c9d40ef2b0f2e697f0d1973fc9e24ae9401cf41698612bbebc"
|
|
||||||
name = "github.com/creack/pty"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = "UT"
|
|
||||||
revision = "2769f65a3a94eb8f876f44a0459d24ae7ad2e488"
|
|
||||||
version = "v1.1.7"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
|
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
|
||||||
name = "github.com/davecgh/go-spew"
|
name = "github.com/davecgh/go-spew"
|
||||||
|
@ -193,14 +177,6 @@
|
||||||
pruneopts = "UT"
|
pruneopts = "UT"
|
||||||
revision = "ed7bcb39ff10f39ab08e317ce16df282845852fa"
|
revision = "ed7bcb39ff10f39ab08e317ce16df282845852fa"
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:0a01355005024757ae2a1f62e8fe68a30b4f5fde7a08ac88b3685e7999ac354b"
|
|
||||||
name = "github.com/gliderlabs/ssh"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = "UT"
|
|
||||||
revision = "5b6cc7030f17095c0cf23bb063b0bfe824fe5f8b"
|
|
||||||
version = "v0.2.2"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:ec6f9bf5e274c833c911923c9193867f3f18788c461f76f05f62bb1510e0ae65"
|
digest = "1:ec6f9bf5e274c833c911923c9193867f3f18788c461f76f05f62bb1510e0ae65"
|
||||||
name = "github.com/go-sql-driver/mysql"
|
name = "github.com/go-sql-driver/mysql"
|
||||||
|
@ -757,12 +733,10 @@
|
||||||
"github.com/coredns/coredns/request",
|
"github.com/coredns/coredns/request",
|
||||||
"github.com/coreos/go-oidc",
|
"github.com/coreos/go-oidc",
|
||||||
"github.com/coreos/go-systemd/daemon",
|
"github.com/coreos/go-systemd/daemon",
|
||||||
"github.com/creack/pty",
|
|
||||||
"github.com/denisenkom/go-mssqldb",
|
"github.com/denisenkom/go-mssqldb",
|
||||||
"github.com/equinox-io/equinox",
|
"github.com/equinox-io/equinox",
|
||||||
"github.com/facebookgo/grace/gracenet",
|
"github.com/facebookgo/grace/gracenet",
|
||||||
"github.com/getsentry/raven-go",
|
"github.com/getsentry/raven-go",
|
||||||
"github.com/gliderlabs/ssh",
|
|
||||||
"github.com/go-sql-driver/mysql",
|
"github.com/go-sql-driver/mysql",
|
||||||
"github.com/golang-collections/collections/queue",
|
"github.com/golang-collections/collections/queue",
|
||||||
"github.com/google/uuid",
|
"github.com/google/uuid",
|
||||||
|
|
|
@ -115,11 +115,3 @@
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
branch = "v2"
|
branch = "v2"
|
||||||
name = "github.com/coreos/go-oidc"
|
name = "github.com/coreos/go-oidc"
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/gliderlabs/ssh"
|
|
||||||
version = "0.2.2"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/creack/pty"
|
|
||||||
version = "1.1.7"
|
|
||||||
|
|
|
@ -1,9 +1,3 @@
|
||||||
2019.8.2
|
|
||||||
- 2019-08-20 STOR-519: Add db-connect, a SQL over HTTPS server
|
|
||||||
- 2019-08-11 TUN-2163: Add GrapQLType method to Scope interface
|
|
||||||
- 2019-08-06 TUN-2152: Requests with a query in the URL are erroneously escaped
|
|
||||||
- 2019-07-18 AUTH-1941: Adds initial SSH server implementation
|
|
||||||
|
|
||||||
2019.8.1
|
2019.8.1
|
||||||
- 2019-08-05 TUN-2111: Implement custom serialization logic for FallibleConfig and OriginConfig
|
- 2019-08-05 TUN-2111: Implement custom serialization logic for FallibleConfig and OriginConfig
|
||||||
- 2019-08-06 Revert "TUN-1736: Missing headers when passing an invalid path"
|
- 2019-08-06 Revert "TUN-1736: Missing headers when passing an invalid path"
|
||||||
|
|
|
@ -8,32 +8,31 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
|
||||||
"runtime/trace"
|
"runtime/trace"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cloudflared/dbconnect"
|
||||||
|
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cloudflared/connection"
|
||||||
|
"github.com/cloudflare/cloudflared/supervisor"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"github.com/getsentry/raven-go"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||||
"github.com/cloudflare/cloudflared/connection"
|
|
||||||
"github.com/cloudflare/cloudflared/dbconnect"
|
|
||||||
"github.com/cloudflare/cloudflared/hello"
|
"github.com/cloudflare/cloudflared/hello"
|
||||||
"github.com/cloudflare/cloudflared/metrics"
|
"github.com/cloudflare/cloudflared/metrics"
|
||||||
"github.com/cloudflare/cloudflared/origin"
|
"github.com/cloudflare/cloudflared/origin"
|
||||||
"github.com/cloudflare/cloudflared/signal"
|
"github.com/cloudflare/cloudflared/signal"
|
||||||
"github.com/cloudflare/cloudflared/sshserver"
|
|
||||||
"github.com/cloudflare/cloudflared/supervisor"
|
|
||||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||||
"github.com/cloudflare/cloudflared/tunneldns"
|
"github.com/cloudflare/cloudflared/tunneldns"
|
||||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
|
||||||
"github.com/cloudflare/cloudflared/websocket"
|
"github.com/cloudflare/cloudflared/websocket"
|
||||||
|
|
||||||
"github.com/coreos/go-systemd/daemon"
|
"github.com/coreos/go-systemd/daemon"
|
||||||
"github.com/facebookgo/grace/gracenet"
|
"github.com/facebookgo/grace/gracenet"
|
||||||
"github.com/getsentry/raven-go"
|
|
||||||
"github.com/gliderlabs/ssh"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/urfave/cli.v2"
|
"gopkg.in/urfave/cli.v2"
|
||||||
"gopkg.in/urfave/cli.v2/altsrc"
|
"gopkg.in/urfave/cli.v2/altsrc"
|
||||||
|
@ -291,31 +290,6 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||||
c.Set("url", "https://"+helloListener.Addr().String())
|
c.Set("url", "https://"+helloListener.Addr().String())
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.IsSet("ssh-server") {
|
|
||||||
if runtime.GOOS != "darwin" && runtime.GOOS != "linux" {
|
|
||||||
logger.Errorf("--ssh-server is not supported on %s", runtime.GOOS)
|
|
||||||
return errors.New(fmt.Sprintf("--ssh-server is not supported on %s", runtime.GOOS))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Infof("ssh-server set")
|
|
||||||
|
|
||||||
sshServerAddress := "127.0.0.1:" + c.String("local-ssh-port")
|
|
||||||
server, err := sshserver.New(logger, sshServerAddress, shutdownC)
|
|
||||||
if err != nil {
|
|
||||||
logger.WithError(err).Error("Cannot create new SSH Server")
|
|
||||||
return errors.Wrap(err, "Cannot create new SSH Server")
|
|
||||||
}
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
if err = server.Start(); err != nil && err != ssh.ErrServerClosed {
|
|
||||||
logger.WithError(err).Error("SSH server error")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
c.Set("url", "ssh://"+sshServerAddress)
|
|
||||||
}
|
|
||||||
|
|
||||||
if host := hostnameFromURI(c.String("url")); host != "" {
|
if host := hostnameFromURI(c.String("url")); host != "" {
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:")
|
listener, err := net.Listen("tcp", "127.0.0.1:")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -339,7 +313,6 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
errC <- origin.StartTunnelDaemon(ctx, tunnelConfig, connectedSignal, cloudflaredID)
|
errC <- origin.StartTunnelDaemon(ctx, tunnelConfig, connectedSignal, cloudflaredID)
|
||||||
errC <- origin.StartTunnelDaemon(ctx, tunnelConfig, connectedSignal, cloudflaredID)
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"))
|
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"))
|
||||||
|
@ -776,13 +749,6 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||||
EnvVars: []string{"TUNNEL_HELLO_WORLD"},
|
EnvVars: []string{"TUNNEL_HELLO_WORLD"},
|
||||||
Hidden: shouldHide,
|
Hidden: shouldHide,
|
||||||
}),
|
}),
|
||||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
|
||||||
Name: "ssh-server",
|
|
||||||
Value: false,
|
|
||||||
Usage: "Run an SSH Server",
|
|
||||||
EnvVars: []string{"TUNNEL_SSH_SERVER"},
|
|
||||||
Hidden: true, // TODO: remove when feature is complete
|
|
||||||
}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{
|
altsrc.NewStringFlag(&cli.StringFlag{
|
||||||
Name: "pidfile",
|
Name: "pidfile",
|
||||||
Usage: "Write the application's PID to this file after first successful connection.",
|
Usage: "Write the application's PID to this file after first successful connection.",
|
||||||
|
@ -925,12 +891,5 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||||
EnvVars: []string{"DIAL_EDGE_TIMEOUT"},
|
EnvVars: []string{"DIAL_EDGE_TIMEOUT"},
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
}),
|
}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{
|
|
||||||
Name: "local-ssh-port",
|
|
||||||
Usage: "Localhost port that cloudflared SSH server will run on",
|
|
||||||
Value: "22",
|
|
||||||
EnvVars: []string{"LOCAL_SSH_PORT"},
|
|
||||||
Hidden: true,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,168 +0,0 @@
|
||||||
package sshserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"fmt"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/user"
|
|
||||||
"strconv"
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/creack/pty"
|
|
||||||
"github.com/gliderlabs/ssh"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SSHServer struct {
|
|
||||||
ssh.Server
|
|
||||||
logger *logrus.Logger
|
|
||||||
shutdownC chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
const DefaultShellPrompt = `\e[0;31m[\u@\h \W]\$ \e[m `
|
|
||||||
|
|
||||||
func New(logger *logrus.Logger, address string, shutdownC chan struct{}) (*SSHServer, error) {
|
|
||||||
currentUser, err := user.Current()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if currentUser.Uid != "0" {
|
|
||||||
return nil, errors.New("cloudflared ssh server needs to run as root")
|
|
||||||
}
|
|
||||||
|
|
||||||
sshServer := SSHServer{ssh.Server{Addr: address}, logger, shutdownC}
|
|
||||||
return &sshServer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SSHServer) Start() error {
|
|
||||||
s.logger.Infof("Starting SSH server at %s", s.Addr)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
<-s.shutdownC
|
|
||||||
if err := s.Close(); err != nil {
|
|
||||||
s.logger.WithError(err).Error("Cannot close SSH server")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
s.Handle(s.connectionHandler)
|
|
||||||
return s.ListenAndServe()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SSHServer) connectionHandler(session ssh.Session) {
|
|
||||||
|
|
||||||
// Get uid and gid of user attempting to login
|
|
||||||
uid, gid, err := getUser(session.User())
|
|
||||||
if err != nil {
|
|
||||||
if _, err := io.WriteString(session, "Invalid credentials\n"); err != nil {
|
|
||||||
s.logger.WithError(err).Error("Invalid credentials: Failed to write to SSH session")
|
|
||||||
}
|
|
||||||
if err := session.Exit(1); err != nil {
|
|
||||||
s.logger.WithError(err).Error("Failed to close SSH session")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spawn shell under user
|
|
||||||
cmd := exec.Command("/bin/bash")
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: uid, Gid: gid}}
|
|
||||||
|
|
||||||
ptyReq, winCh, isPty := session.Pty()
|
|
||||||
if !isPty {
|
|
||||||
if _, err := io.WriteString(session, "No PTY requested.\n"); err != nil {
|
|
||||||
s.logger.WithError(err).Error("No PTY requested: Failed to write to SSH session")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := session.Exit(1); err != nil {
|
|
||||||
s.logger.WithError(err).Error("Failed to close SSH session")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term))
|
|
||||||
cmd.Env = append(cmd.Env, fmt.Sprintf("PS1=%s", DefaultShellPrompt))
|
|
||||||
psuedoTTY, err := pty.Start(cmd)
|
|
||||||
if err != nil {
|
|
||||||
s.logger.WithError(err).Error("Failed to start pty session")
|
|
||||||
if err := session.Exit(1); err != nil {
|
|
||||||
s.logger.WithError(err).Error("Failed to close SSH session")
|
|
||||||
}
|
|
||||||
close(s.shutdownC)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle terminal window size changes
|
|
||||||
go func() {
|
|
||||||
for win := range winCh {
|
|
||||||
if errNo := setWinsize(psuedoTTY, win.Width, win.Height); errNo != 0 {
|
|
||||||
s.logger.WithError(err).Error("Failed to set pty window size: ", err.Error())
|
|
||||||
if err := session.Exit(1); err != nil {
|
|
||||||
s.logger.WithError(err).Error("Failed to close SSH session")
|
|
||||||
}
|
|
||||||
close(s.shutdownC)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Write incoming commands to PTY
|
|
||||||
go func() {
|
|
||||||
if _, err := io.Copy(psuedoTTY, session); err != nil {
|
|
||||||
s.logger.WithError(err).Error("Failed to write incoming command to pty")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
pr, pw := io.Pipe()
|
|
||||||
scanner := bufio.NewScanner(pr)
|
|
||||||
go func() {
|
|
||||||
for scanner.Scan() {
|
|
||||||
s.logger.Info(scanner.Text())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Write outgoing command output to both the command recorder, and remote user
|
|
||||||
mw := io.MultiWriter(pw, session)
|
|
||||||
if _, err := io.Copy(mw, psuedoTTY); err != nil {
|
|
||||||
s.logger.WithError(err).Error("Failed to write command output to user")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := pw.Close(); err != nil {
|
|
||||||
s.logger.WithError(err).Error("Failed to close pipe writer")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := pr.Close(); err != nil {
|
|
||||||
s.logger.WithError(err).Error("Failed to close pipe reader")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all resources associated with cmd to be released
|
|
||||||
// Returns error if shell exited with a non-zero status or received a signal
|
|
||||||
if err := cmd.Wait(); err != nil {
|
|
||||||
s.logger.WithError(err).Debug("Shell did not close correctly")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sets PTY window size for terminal
|
|
||||||
func setWinsize(f *os.File, w, h int) syscall.Errno {
|
|
||||||
_, _, errNo := 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})))
|
|
||||||
return errNo
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only works on POSIX systems
|
|
||||||
func getUser(username string) (uint32, uint32, error) {
|
|
||||||
sshUser, err := user.Lookup(username)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
uid, err := strconv.ParseUint(sshUser.Uid, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
gid, err := strconv.ParseUint(sshUser.Gid, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
return uint32(uid), uint32(gid), nil
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
shlex.test
|
|
|
@ -1,20 +0,0 @@
|
||||||
Copyright (c) anmitsu <anmitsu.s@gmail.com>
|
|
||||||
|
|
||||||
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.
|
|
|
@ -1,38 +0,0 @@
|
||||||
# go-shlex
|
|
||||||
|
|
||||||
go-shlex is a library to make a lexical analyzer like Unix shell for
|
|
||||||
Go.
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
go get -u "github.com/anmitsu/go-shlex"
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/anmitsu/go-shlex"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
cmd := `cp -Rdp "file name" 'file name2' dir\ name`
|
|
||||||
words, err := shlex.Split(cmd, true)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, w := range words {
|
|
||||||
fmt.Println(w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
http://godoc.org/github.com/anmitsu/go-shlex
|
|
||||||
|
|
|
@ -1,193 +0,0 @@
|
||||||
// Package shlex provides a simple lexical analysis like Unix shell.
|
|
||||||
package shlex
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrNoClosing = errors.New("No closing quotation")
|
|
||||||
ErrNoEscaped = errors.New("No escaped character")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tokenizer is the interface that classifies a token according to
|
|
||||||
// words, whitespaces, quotations, escapes and escaped quotations.
|
|
||||||
type Tokenizer interface {
|
|
||||||
IsWord(rune) bool
|
|
||||||
IsWhitespace(rune) bool
|
|
||||||
IsQuote(rune) bool
|
|
||||||
IsEscape(rune) bool
|
|
||||||
IsEscapedQuote(rune) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultTokenizer implements a simple tokenizer like Unix shell.
|
|
||||||
type DefaultTokenizer struct{}
|
|
||||||
|
|
||||||
func (t *DefaultTokenizer) IsWord(r rune) bool {
|
|
||||||
return r == '_' || unicode.IsLetter(r) || unicode.IsNumber(r)
|
|
||||||
}
|
|
||||||
func (t *DefaultTokenizer) IsQuote(r rune) bool {
|
|
||||||
switch r {
|
|
||||||
case '\'', '"':
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (t *DefaultTokenizer) IsWhitespace(r rune) bool {
|
|
||||||
return unicode.IsSpace(r)
|
|
||||||
}
|
|
||||||
func (t *DefaultTokenizer) IsEscape(r rune) bool {
|
|
||||||
return r == '\\'
|
|
||||||
}
|
|
||||||
func (t *DefaultTokenizer) IsEscapedQuote(r rune) bool {
|
|
||||||
return r == '"'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lexer represents a lexical analyzer.
|
|
||||||
type Lexer struct {
|
|
||||||
reader *bufio.Reader
|
|
||||||
tokenizer Tokenizer
|
|
||||||
posix bool
|
|
||||||
whitespacesplit bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLexer creates a new Lexer reading from io.Reader. This Lexer
|
|
||||||
// has a DefaultTokenizer according to posix and whitespacesplit
|
|
||||||
// rules.
|
|
||||||
func NewLexer(r io.Reader, posix, whitespacesplit bool) *Lexer {
|
|
||||||
return &Lexer{
|
|
||||||
reader: bufio.NewReader(r),
|
|
||||||
tokenizer: &DefaultTokenizer{},
|
|
||||||
posix: posix,
|
|
||||||
whitespacesplit: whitespacesplit,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLexerString creates a new Lexer reading from a string. This
|
|
||||||
// Lexer has a DefaultTokenizer according to posix and whitespacesplit
|
|
||||||
// rules.
|
|
||||||
func NewLexerString(s string, posix, whitespacesplit bool) *Lexer {
|
|
||||||
return NewLexer(strings.NewReader(s), posix, whitespacesplit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Split splits a string according to posix or non-posix rules.
|
|
||||||
func Split(s string, posix bool) ([]string, error) {
|
|
||||||
return NewLexerString(s, posix, true).Split()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetTokenizer sets a Tokenizer.
|
|
||||||
func (l *Lexer) SetTokenizer(t Tokenizer) {
|
|
||||||
l.tokenizer = t
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Lexer) Split() ([]string, error) {
|
|
||||||
result := make([]string, 0)
|
|
||||||
for {
|
|
||||||
token, err := l.readToken()
|
|
||||||
if token != "" {
|
|
||||||
result = append(result, token)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
} else if err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Lexer) readToken() (string, error) {
|
|
||||||
t := l.tokenizer
|
|
||||||
token := ""
|
|
||||||
quoted := false
|
|
||||||
state := ' '
|
|
||||||
escapedstate := ' '
|
|
||||||
scanning:
|
|
||||||
for {
|
|
||||||
next, _, err := l.reader.ReadRune()
|
|
||||||
if err != nil {
|
|
||||||
if t.IsQuote(state) {
|
|
||||||
return token, ErrNoClosing
|
|
||||||
} else if t.IsEscape(state) {
|
|
||||||
return token, ErrNoEscaped
|
|
||||||
}
|
|
||||||
return token, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case t.IsWhitespace(state):
|
|
||||||
switch {
|
|
||||||
case t.IsWhitespace(next):
|
|
||||||
break scanning
|
|
||||||
case l.posix && t.IsEscape(next):
|
|
||||||
escapedstate = 'a'
|
|
||||||
state = next
|
|
||||||
case t.IsWord(next):
|
|
||||||
token += string(next)
|
|
||||||
state = 'a'
|
|
||||||
case t.IsQuote(next):
|
|
||||||
if !l.posix {
|
|
||||||
token += string(next)
|
|
||||||
}
|
|
||||||
state = next
|
|
||||||
default:
|
|
||||||
token = string(next)
|
|
||||||
if l.whitespacesplit {
|
|
||||||
state = 'a'
|
|
||||||
} else if token != "" || (l.posix && quoted) {
|
|
||||||
break scanning
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case t.IsQuote(state):
|
|
||||||
quoted = true
|
|
||||||
switch {
|
|
||||||
case next == state:
|
|
||||||
if !l.posix {
|
|
||||||
token += string(next)
|
|
||||||
break scanning
|
|
||||||
} else {
|
|
||||||
state = 'a'
|
|
||||||
}
|
|
||||||
case l.posix && t.IsEscape(next) && t.IsEscapedQuote(state):
|
|
||||||
escapedstate = state
|
|
||||||
state = next
|
|
||||||
default:
|
|
||||||
token += string(next)
|
|
||||||
}
|
|
||||||
case t.IsEscape(state):
|
|
||||||
if t.IsQuote(escapedstate) && next != state && next != escapedstate {
|
|
||||||
token += string(state)
|
|
||||||
}
|
|
||||||
token += string(next)
|
|
||||||
state = escapedstate
|
|
||||||
case t.IsWord(state):
|
|
||||||
switch {
|
|
||||||
case t.IsWhitespace(next):
|
|
||||||
if token != "" || (l.posix && quoted) {
|
|
||||||
break scanning
|
|
||||||
}
|
|
||||||
case l.posix && t.IsQuote(next):
|
|
||||||
state = next
|
|
||||||
case l.posix && t.IsEscape(next):
|
|
||||||
escapedstate = 'a'
|
|
||||||
state = next
|
|
||||||
case t.IsWord(next) || t.IsQuote(next):
|
|
||||||
token += string(next)
|
|
||||||
default:
|
|
||||||
if l.whitespacesplit {
|
|
||||||
token += string(next)
|
|
||||||
} else if token != "" {
|
|
||||||
l.reader.UnreadRune()
|
|
||||||
break scanning
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return token, nil
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
[568].out
|
|
||||||
_go*
|
|
||||||
_test*
|
|
||||||
_obj
|
|
|
@ -1,14 +0,0 @@
|
||||||
FROM golang:1.12
|
|
||||||
|
|
||||||
# Clone and complie a riscv compatible version of the go compiler.
|
|
||||||
RUN git clone https://review.gerrithub.io/riscv/riscv-go /riscv-go
|
|
||||||
# riscvdev branch HEAD as of 2019-06-29.
|
|
||||||
RUN cd /riscv-go && git checkout 04885fddd096d09d4450726064d06dd107e374bf
|
|
||||||
ENV PATH=/riscv-go/misc/riscv:/riscv-go/bin:$PATH
|
|
||||||
RUN cd /riscv-go/src && GOROOT_BOOTSTRAP=$(go env GOROOT) ./make.bash
|
|
||||||
ENV GOROOT=/riscv-go
|
|
||||||
|
|
||||||
# Make sure we compile.
|
|
||||||
WORKDIR pty
|
|
||||||
ADD . .
|
|
||||||
RUN GOOS=linux GOARCH=riscv go build
|
|
|
@ -1,23 +0,0 @@
|
||||||
Copyright (c) 2011 Keith Rarick
|
|
||||||
|
|
||||||
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.
|
|
|
@ -1,100 +0,0 @@
|
||||||
# pty
|
|
||||||
|
|
||||||
Pty is a Go package for using unix pseudo-terminals.
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
go get github.com/creack/pty
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
### Command
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/creack/pty"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := exec.Command("grep", "--color=auto", "bar")
|
|
||||||
f, err := pty.Start(c)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
f.Write([]byte("foo\n"))
|
|
||||||
f.Write([]byte("bar\n"))
|
|
||||||
f.Write([]byte("baz\n"))
|
|
||||||
f.Write([]byte{4}) // EOT
|
|
||||||
}()
|
|
||||||
io.Copy(os.Stdout, f)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Shell
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/creack/pty"
|
|
||||||
"golang.org/x/crypto/ssh/terminal"
|
|
||||||
)
|
|
||||||
|
|
||||||
func test() error {
|
|
||||||
// Create arbitrary command.
|
|
||||||
c := exec.Command("bash")
|
|
||||||
|
|
||||||
// Start the command with a pty.
|
|
||||||
ptmx, err := pty.Start(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Make sure to close the pty at the end.
|
|
||||||
defer func() { _ = ptmx.Close() }() // Best effort.
|
|
||||||
|
|
||||||
// Handle pty size.
|
|
||||||
ch := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(ch, syscall.SIGWINCH)
|
|
||||||
go func() {
|
|
||||||
for range ch {
|
|
||||||
if err := pty.InheritSize(os.Stdin, ptmx); err != nil {
|
|
||||||
log.Printf("error resizing pty: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
ch <- syscall.SIGWINCH // Initial resize.
|
|
||||||
|
|
||||||
// Set stdin in raw mode.
|
|
||||||
oldState, err := terminal.MakeRaw(int(os.Stdin.Fd()))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer func() { _ = terminal.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort.
|
|
||||||
|
|
||||||
// Copy stdin to the pty and the pty to stdout.
|
|
||||||
go func() { _, _ = io.Copy(ptmx, os.Stdin) }()
|
|
||||||
_, _ = io.Copy(os.Stdout, ptmx)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if err := test(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
|
@ -1,16 +0,0 @@
|
||||||
// Package pty provides functions for working with Unix terminals.
|
|
||||||
package pty
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrUnsupported is returned if a function is not
|
|
||||||
// available on the current platform.
|
|
||||||
var ErrUnsupported = errors.New("unsupported")
|
|
||||||
|
|
||||||
// Opens a pty and its corresponding tty.
|
|
||||||
func Open() (pty, tty *os.File, err error) {
|
|
||||||
return open()
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
// +build !windows,!solaris
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
import "syscall"
|
|
||||||
|
|
||||||
func ioctl(fd, cmd, ptr uintptr) error {
|
|
||||||
_, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, cmd, ptr)
|
|
||||||
if e != 0 {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
// +build darwin dragonfly freebsd netbsd openbsd
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
// from <sys/ioccom.h>
|
|
||||||
const (
|
|
||||||
_IOC_VOID uintptr = 0x20000000
|
|
||||||
_IOC_OUT uintptr = 0x40000000
|
|
||||||
_IOC_IN uintptr = 0x80000000
|
|
||||||
_IOC_IN_OUT uintptr = _IOC_OUT | _IOC_IN
|
|
||||||
_IOC_DIRMASK = _IOC_VOID | _IOC_OUT | _IOC_IN
|
|
||||||
|
|
||||||
_IOC_PARAM_SHIFT = 13
|
|
||||||
_IOC_PARAM_MASK = (1 << _IOC_PARAM_SHIFT) - 1
|
|
||||||
)
|
|
||||||
|
|
||||||
func _IOC_PARM_LEN(ioctl uintptr) uintptr {
|
|
||||||
return (ioctl >> 16) & _IOC_PARAM_MASK
|
|
||||||
}
|
|
||||||
|
|
||||||
func _IOC(inout uintptr, group byte, ioctl_num uintptr, param_len uintptr) uintptr {
|
|
||||||
return inout | (param_len&_IOC_PARAM_MASK)<<16 | uintptr(group)<<8 | ioctl_num
|
|
||||||
}
|
|
||||||
|
|
||||||
func _IO(group byte, ioctl_num uintptr) uintptr {
|
|
||||||
return _IOC(_IOC_VOID, group, ioctl_num, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _IOR(group byte, ioctl_num uintptr, param_len uintptr) uintptr {
|
|
||||||
return _IOC(_IOC_OUT, group, ioctl_num, param_len)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _IOW(group byte, ioctl_num uintptr, param_len uintptr) uintptr {
|
|
||||||
return _IOC(_IOC_IN, group, ioctl_num, param_len)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _IOWR(group byte, ioctl_num uintptr, param_len uintptr) uintptr {
|
|
||||||
return _IOC(_IOC_IN_OUT, group, ioctl_num, param_len)
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
package pty
|
|
||||||
|
|
||||||
import (
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// see /usr/include/sys/stropts.h
|
|
||||||
I_PUSH = uintptr((int32('S')<<8 | 002))
|
|
||||||
I_STR = uintptr((int32('S')<<8 | 010))
|
|
||||||
I_FIND = uintptr((int32('S')<<8 | 013))
|
|
||||||
// see /usr/include/sys/ptms.h
|
|
||||||
ISPTM = (int32('P') << 8) | 1
|
|
||||||
UNLKPT = (int32('P') << 8) | 2
|
|
||||||
PTSSTTY = (int32('P') << 8) | 3
|
|
||||||
ZONEPT = (int32('P') << 8) | 4
|
|
||||||
OWNERPT = (int32('P') << 8) | 5
|
|
||||||
)
|
|
||||||
|
|
||||||
type strioctl struct {
|
|
||||||
ic_cmd int32
|
|
||||||
ic_timout int32
|
|
||||||
ic_len int32
|
|
||||||
ic_dp unsafe.Pointer
|
|
||||||
}
|
|
||||||
|
|
||||||
func ioctl(fd, cmd, ptr uintptr) error {
|
|
||||||
return unix.IoctlSetInt(int(fd), uint(cmd), int(ptr))
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
GOOSARCH="${GOOS}_${GOARCH}"
|
|
||||||
case "$GOOSARCH" in
|
|
||||||
_* | *_ | _)
|
|
||||||
echo 'undefined $GOOS_$GOARCH:' "$GOOSARCH" 1>&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
GODEFS="go tool cgo -godefs"
|
|
||||||
|
|
||||||
$GODEFS types.go |gofmt > ztypes_$GOARCH.go
|
|
||||||
|
|
||||||
case $GOOS in
|
|
||||||
freebsd|dragonfly|openbsd)
|
|
||||||
$GODEFS types_$GOOS.go |gofmt > ztypes_$GOOSARCH.go
|
|
||||||
;;
|
|
||||||
esac
|
|
|
@ -1,65 +0,0 @@
|
||||||
package pty
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
func open() (pty, tty *os.File, err error) {
|
|
||||||
pFD, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_CLOEXEC, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
p := os.NewFile(uintptr(pFD), "/dev/ptmx")
|
|
||||||
// In case of error after this point, make sure we close the ptmx fd.
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
_ = p.Close() // Best effort.
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
sname, err := ptsname(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := grantpt(p); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := unlockpt(p); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
t, err := os.OpenFile(sname, os.O_RDWR, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
return p, t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ptsname(f *os.File) (string, error) {
|
|
||||||
n := make([]byte, _IOC_PARM_LEN(syscall.TIOCPTYGNAME))
|
|
||||||
|
|
||||||
err := ioctl(f.Fd(), syscall.TIOCPTYGNAME, uintptr(unsafe.Pointer(&n[0])))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, c := range n {
|
|
||||||
if c == 0 {
|
|
||||||
return string(n[:i]), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", errors.New("TIOCPTYGNAME string not NUL-terminated")
|
|
||||||
}
|
|
||||||
|
|
||||||
func grantpt(f *os.File) error {
|
|
||||||
return ioctl(f.Fd(), syscall.TIOCPTYGRANT, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func unlockpt(f *os.File) error {
|
|
||||||
return ioctl(f.Fd(), syscall.TIOCPTYUNLK, 0)
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
package pty
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
// same code as pty_darwin.go
|
|
||||||
func open() (pty, tty *os.File, err error) {
|
|
||||||
p, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
// In case of error after this point, make sure we close the ptmx fd.
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
_ = p.Close() // Best effort.
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
sname, err := ptsname(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := grantpt(p); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := unlockpt(p); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
t, err := os.OpenFile(sname, os.O_RDWR, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
return p, t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func grantpt(f *os.File) error {
|
|
||||||
_, err := isptmaster(f.Fd())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func unlockpt(f *os.File) error {
|
|
||||||
_, err := isptmaster(f.Fd())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func isptmaster(fd uintptr) (bool, error) {
|
|
||||||
err := ioctl(fd, syscall.TIOCISPTMASTER, 0)
|
|
||||||
return err == nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
emptyFiodgnameArg fiodgnameArg
|
|
||||||
ioctl_FIODNAME = _IOW('f', 120, unsafe.Sizeof(emptyFiodgnameArg))
|
|
||||||
)
|
|
||||||
|
|
||||||
func ptsname(f *os.File) (string, error) {
|
|
||||||
name := make([]byte, _C_SPECNAMELEN)
|
|
||||||
fa := fiodgnameArg{Name: (*byte)(unsafe.Pointer(&name[0])), Len: _C_SPECNAMELEN, Pad_cgo_0: [4]byte{0, 0, 0, 0}}
|
|
||||||
|
|
||||||
err := ioctl(f.Fd(), ioctl_FIODNAME, uintptr(unsafe.Pointer(&fa)))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, c := range name {
|
|
||||||
if c == 0 {
|
|
||||||
s := "/dev/" + string(name[:i])
|
|
||||||
return strings.Replace(s, "ptm", "pts", -1), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", errors.New("TIOCPTYGNAME string not NUL-terminated")
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
package pty
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
func posixOpenpt(oflag int) (fd int, err error) {
|
|
||||||
r0, _, e1 := syscall.Syscall(syscall.SYS_POSIX_OPENPT, uintptr(oflag), 0, 0)
|
|
||||||
fd = int(r0)
|
|
||||||
if e1 != 0 {
|
|
||||||
err = e1
|
|
||||||
}
|
|
||||||
return fd, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func open() (pty, tty *os.File, err error) {
|
|
||||||
fd, err := posixOpenpt(syscall.O_RDWR | syscall.O_CLOEXEC)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
p := os.NewFile(uintptr(fd), "/dev/pts")
|
|
||||||
// In case of error after this point, make sure we close the pts fd.
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
_ = p.Close() // Best effort.
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
sname, err := ptsname(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
t, err := os.OpenFile("/dev/"+sname, os.O_RDWR, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
return p, t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isptmaster(fd uintptr) (bool, error) {
|
|
||||||
err := ioctl(fd, syscall.TIOCPTMASTER, 0)
|
|
||||||
return err == nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
emptyFiodgnameArg fiodgnameArg
|
|
||||||
ioctlFIODGNAME = _IOW('f', 120, unsafe.Sizeof(emptyFiodgnameArg))
|
|
||||||
)
|
|
||||||
|
|
||||||
func ptsname(f *os.File) (string, error) {
|
|
||||||
master, err := isptmaster(f.Fd())
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if !master {
|
|
||||||
return "", syscall.EINVAL
|
|
||||||
}
|
|
||||||
|
|
||||||
const n = _C_SPECNAMELEN + 1
|
|
||||||
var (
|
|
||||||
buf = make([]byte, n)
|
|
||||||
arg = fiodgnameArg{Len: n, Buf: (*byte)(unsafe.Pointer(&buf[0]))}
|
|
||||||
)
|
|
||||||
if err := ioctl(f.Fd(), ioctlFIODGNAME, uintptr(unsafe.Pointer(&arg))); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, c := range buf {
|
|
||||||
if c == 0 {
|
|
||||||
return string(buf[:i]), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", errors.New("FIODGNAME string not NUL-terminated")
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
package pty
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
func open() (pty, tty *os.File, err error) {
|
|
||||||
p, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
// In case of error after this point, make sure we close the ptmx fd.
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
_ = p.Close() // Best effort.
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
sname, err := ptsname(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := unlockpt(p); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
t, err := os.OpenFile(sname, os.O_RDWR|syscall.O_NOCTTY, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
return p, t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ptsname(f *os.File) (string, error) {
|
|
||||||
var n _C_uint
|
|
||||||
err := ioctl(f.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n)))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return "/dev/pts/" + strconv.Itoa(int(n)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func unlockpt(f *os.File) error {
|
|
||||||
var u _C_int
|
|
||||||
// use TIOCSPTLCK with a pointer to zero to clear the lock
|
|
||||||
return ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u)))
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
package pty
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
func open() (pty, tty *os.File, err error) {
|
|
||||||
/*
|
|
||||||
* from ptm(4):
|
|
||||||
* The PTMGET command allocates a free pseudo terminal, changes its
|
|
||||||
* ownership to the caller, revokes the access privileges for all previous
|
|
||||||
* users, opens the file descriptors for the pty and tty devices and
|
|
||||||
* returns them to the caller in struct ptmget.
|
|
||||||
*/
|
|
||||||
|
|
||||||
p, err := os.OpenFile("/dev/ptm", os.O_RDWR|syscall.O_CLOEXEC, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
defer p.Close()
|
|
||||||
|
|
||||||
var ptm ptmget
|
|
||||||
if err := ioctl(p.Fd(), uintptr(ioctl_PTMGET), uintptr(unsafe.Pointer(&ptm))); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pty = os.NewFile(uintptr(ptm.Cfd), "/dev/ptm")
|
|
||||||
tty = os.NewFile(uintptr(ptm.Sfd), "/dev/ptm")
|
|
||||||
|
|
||||||
return pty, tty, nil
|
|
||||||
}
|
|
|
@ -1,139 +0,0 @@
|
||||||
package pty
|
|
||||||
|
|
||||||
/* based on:
|
|
||||||
http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libc/port/gen/pt.c
|
|
||||||
*/
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
const NODEV = ^uint64(0)
|
|
||||||
|
|
||||||
func open() (pty, tty *os.File, err error) {
|
|
||||||
masterfd, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|unix.O_NOCTTY, 0)
|
|
||||||
//masterfd, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_CLOEXEC|unix.O_NOCTTY, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
p := os.NewFile(uintptr(masterfd), "/dev/ptmx")
|
|
||||||
|
|
||||||
sname, err := ptsname(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = grantpt(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = unlockpt(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
slavefd, err := syscall.Open(sname, os.O_RDWR|unix.O_NOCTTY, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
t := os.NewFile(uintptr(slavefd), sname)
|
|
||||||
|
|
||||||
// pushing terminal driver STREAMS modules as per pts(7)
|
|
||||||
for _, mod := range([]string{"ptem", "ldterm", "ttcompat"}) {
|
|
||||||
err = streams_push(t, mod)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return p, t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func minor(x uint64) uint64 {
|
|
||||||
return x & 0377
|
|
||||||
}
|
|
||||||
|
|
||||||
func ptsdev(fd uintptr) uint64 {
|
|
||||||
istr := strioctl{ISPTM, 0, 0, nil}
|
|
||||||
err := ioctl(fd, I_STR, uintptr(unsafe.Pointer(&istr)))
|
|
||||||
if err != nil {
|
|
||||||
return NODEV
|
|
||||||
}
|
|
||||||
var status unix.Stat_t
|
|
||||||
err = unix.Fstat(int(fd), &status)
|
|
||||||
if err != nil {
|
|
||||||
return NODEV
|
|
||||||
}
|
|
||||||
return uint64(minor(status.Rdev))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ptsname(f *os.File) (string, error) {
|
|
||||||
dev := ptsdev(f.Fd())
|
|
||||||
if dev == NODEV {
|
|
||||||
return "", errors.New("not a master pty")
|
|
||||||
}
|
|
||||||
fn := "/dev/pts/" + strconv.FormatInt(int64(dev), 10)
|
|
||||||
// access(2) creates the slave device (if the pty exists)
|
|
||||||
// F_OK == 0 (unistd.h)
|
|
||||||
err := unix.Access(fn, 0)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return fn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type pt_own struct {
|
|
||||||
pto_ruid int32
|
|
||||||
pto_rgid int32
|
|
||||||
}
|
|
||||||
|
|
||||||
func grantpt(f *os.File) error {
|
|
||||||
if ptsdev(f.Fd()) == NODEV {
|
|
||||||
return errors.New("not a master pty")
|
|
||||||
}
|
|
||||||
var pto pt_own
|
|
||||||
pto.pto_ruid = int32(os.Getuid())
|
|
||||||
// XXX should first attempt to get gid of DEFAULT_TTY_GROUP="tty"
|
|
||||||
pto.pto_rgid = int32(os.Getgid())
|
|
||||||
var istr strioctl
|
|
||||||
istr.ic_cmd = OWNERPT
|
|
||||||
istr.ic_timout = 0
|
|
||||||
istr.ic_len = int32(unsafe.Sizeof(istr))
|
|
||||||
istr.ic_dp = unsafe.Pointer(&pto)
|
|
||||||
err := ioctl(f.Fd(), I_STR, uintptr(unsafe.Pointer(&istr)))
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("access denied")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func unlockpt(f *os.File) error {
|
|
||||||
istr := strioctl{UNLKPT, 0, 0, nil}
|
|
||||||
return ioctl(f.Fd(), I_STR, uintptr(unsafe.Pointer(&istr)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// push STREAMS modules if not already done so
|
|
||||||
func streams_push(f *os.File, mod string) error {
|
|
||||||
var err error
|
|
||||||
buf := []byte(mod)
|
|
||||||
// XXX I_FIND is not returning an error when the module
|
|
||||||
// is already pushed even though truss reports a return
|
|
||||||
// value of 1. A bug in the Go Solaris syscall interface?
|
|
||||||
// XXX without this we are at risk of the issue
|
|
||||||
// https://www.illumos.org/issues/9042
|
|
||||||
// but since we are not using libc or XPG4.2, we should not be
|
|
||||||
// double-pushing modules
|
|
||||||
|
|
||||||
err = ioctl(f.Fd(), I_FIND, uintptr(unsafe.Pointer(&buf[0])))
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err = ioctl(f.Fd(), I_PUSH, uintptr(unsafe.Pointer(&buf[0])))
|
|
||||||
return err
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
// +build !linux,!darwin,!freebsd,!dragonfly,!openbsd,!solaris
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func open() (pty, tty *os.File, err error) {
|
|
||||||
return nil, nil, ErrUnsupported
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
// +build !windows
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Start assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout,
|
|
||||||
// and c.Stderr, calls c.Start, and returns the File of the tty's
|
|
||||||
// corresponding pty.
|
|
||||||
func Start(c *exec.Cmd) (pty *os.File, err error) {
|
|
||||||
return StartWithSize(c, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartWithSize assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout,
|
|
||||||
// and c.Stderr, calls c.Start, and returns the File of the tty's
|
|
||||||
// corresponding pty.
|
|
||||||
//
|
|
||||||
// This will resize the pty to the specified size before starting the command
|
|
||||||
func StartWithSize(c *exec.Cmd, sz *Winsize) (pty *os.File, err error) {
|
|
||||||
pty, tty, err := Open()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer tty.Close()
|
|
||||||
if sz != nil {
|
|
||||||
err = Setsize(pty, sz)
|
|
||||||
if err != nil {
|
|
||||||
pty.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if c.Stdout == nil {
|
|
||||||
c.Stdout = tty
|
|
||||||
}
|
|
||||||
if c.Stderr == nil {
|
|
||||||
c.Stderr = tty
|
|
||||||
}
|
|
||||||
if c.Stdin == nil {
|
|
||||||
c.Stdin = tty
|
|
||||||
}
|
|
||||||
if c.SysProcAttr == nil {
|
|
||||||
c.SysProcAttr = &syscall.SysProcAttr{}
|
|
||||||
}
|
|
||||||
c.SysProcAttr.Setctty = true
|
|
||||||
c.SysProcAttr.Setsid = true
|
|
||||||
c.SysProcAttr.Ctty = int(tty.Fd())
|
|
||||||
err = c.Start()
|
|
||||||
if err != nil {
|
|
||||||
pty.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return pty, err
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
# Test script checking that all expected os/arch compile properly.
|
|
||||||
# Does not actually test the logic, just the compilation so we make sure we don't break code depending on the lib.
|
|
||||||
|
|
||||||
echo2() {
|
|
||||||
echo $@ >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
trap end 0
|
|
||||||
end() {
|
|
||||||
[ "$?" = 0 ] && echo2 "Pass." || (echo2 "Fail."; exit 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
cross() {
|
|
||||||
os=$1
|
|
||||||
shift
|
|
||||||
echo2 "Build for $os."
|
|
||||||
for arch in $@; do
|
|
||||||
echo2 " - $os/$arch"
|
|
||||||
GOOS=$os GOARCH=$arch go build
|
|
||||||
done
|
|
||||||
echo2
|
|
||||||
}
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
cross linux amd64 386 arm arm64 ppc64 ppc64le s390x mips mipsle mips64 mips64le
|
|
||||||
cross darwin amd64 386 arm arm64
|
|
||||||
cross freebsd amd64 386 arm
|
|
||||||
cross netbsd amd64 386 arm
|
|
||||||
cross openbsd amd64 386
|
|
||||||
cross dragonfly amd64
|
|
||||||
cross solaris amd64
|
|
||||||
|
|
||||||
# Not expected to work but should still compile.
|
|
||||||
cross windows amd64 386 arm
|
|
||||||
|
|
||||||
# TODO: Fix compilation error on openbsd/arm.
|
|
||||||
# TODO: Merge the solaris PR.
|
|
||||||
|
|
||||||
# Some os/arch require a different compiler. Run in docker.
|
|
||||||
if ! hash docker; then
|
|
||||||
# If docker is not present, stop here.
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo2 "Build for linux."
|
|
||||||
echo2 " - linux/riscv"
|
|
||||||
docker build -t test -f Dockerfile.riscv .
|
|
|
@ -1,10 +0,0 @@
|
||||||
// +build ignore
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
type (
|
|
||||||
_C_int C.int
|
|
||||||
_C_uint C.uint
|
|
||||||
)
|
|
|
@ -1,17 +0,0 @@
|
||||||
// +build ignore
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
/*
|
|
||||||
#define _KERNEL
|
|
||||||
#include <sys/conf.h>
|
|
||||||
#include <sys/param.h>
|
|
||||||
#include <sys/filio.h>
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
const (
|
|
||||||
_C_SPECNAMELEN = C.SPECNAMELEN /* max length of devicename */
|
|
||||||
)
|
|
||||||
|
|
||||||
type fiodgnameArg C.struct_fiodname_args
|
|
|
@ -1,15 +0,0 @@
|
||||||
// +build ignore
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
/*
|
|
||||||
#include <sys/param.h>
|
|
||||||
#include <sys/filio.h>
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
const (
|
|
||||||
_C_SPECNAMELEN = C.SPECNAMELEN /* max length of devicename */
|
|
||||||
)
|
|
||||||
|
|
||||||
type fiodgnameArg C.struct_fiodgname_arg
|
|
|
@ -1,14 +0,0 @@
|
||||||
// +build ignore
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
/*
|
|
||||||
#include <sys/time.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <sys/tty.h>
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
type ptmget C.struct_ptmget
|
|
||||||
|
|
||||||
var ioctl_PTMGET = C.PTMGET
|
|
|
@ -1,64 +0,0 @@
|
||||||
// +build !windows,!solaris
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
// InheritSize applies the terminal size of pty to tty. This should be run
|
|
||||||
// in a signal handler for syscall.SIGWINCH to automatically resize the tty when
|
|
||||||
// the pty receives a window size change notification.
|
|
||||||
func InheritSize(pty, tty *os.File) error {
|
|
||||||
size, err := GetsizeFull(pty)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = Setsize(tty, size)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setsize resizes t to s.
|
|
||||||
func Setsize(t *os.File, ws *Winsize) error {
|
|
||||||
return windowRectCall(ws, t.Fd(), syscall.TIOCSWINSZ)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetsizeFull returns the full terminal size description.
|
|
||||||
func GetsizeFull(t *os.File) (size *Winsize, err error) {
|
|
||||||
var ws Winsize
|
|
||||||
err = windowRectCall(&ws, t.Fd(), syscall.TIOCGWINSZ)
|
|
||||||
return &ws, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getsize returns the number of rows (lines) and cols (positions
|
|
||||||
// in each line) in terminal t.
|
|
||||||
func Getsize(t *os.File) (rows, cols int, err error) {
|
|
||||||
ws, err := GetsizeFull(t)
|
|
||||||
return int(ws.Rows), int(ws.Cols), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Winsize describes the terminal size.
|
|
||||||
type Winsize struct {
|
|
||||||
Rows uint16 // ws_row: Number of rows (in cells)
|
|
||||||
Cols uint16 // ws_col: Number of columns (in cells)
|
|
||||||
X uint16 // ws_xpixel: Width in pixels
|
|
||||||
Y uint16 // ws_ypixel: Height in pixels
|
|
||||||
}
|
|
||||||
|
|
||||||
func windowRectCall(ws *Winsize, fd, a2 uintptr) error {
|
|
||||||
_, _, errno := syscall.Syscall(
|
|
||||||
syscall.SYS_IOCTL,
|
|
||||||
fd,
|
|
||||||
a2,
|
|
||||||
uintptr(unsafe.Pointer(ws)),
|
|
||||||
)
|
|
||||||
if errno != 0 {
|
|
||||||
return syscall.Errno(errno)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
//
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
TIOCGWINSZ = 21608 // 'T' << 8 | 104
|
|
||||||
TIOCSWINSZ = 21607 // 'T' << 8 | 103
|
|
||||||
)
|
|
||||||
|
|
||||||
// Winsize describes the terminal size.
|
|
||||||
type Winsize struct {
|
|
||||||
Rows uint16 // ws_row: Number of rows (in cells)
|
|
||||||
Cols uint16 // ws_col: Number of columns (in cells)
|
|
||||||
X uint16 // ws_xpixel: Width in pixels
|
|
||||||
Y uint16 // ws_ypixel: Height in pixels
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetsizeFull returns the full terminal size description.
|
|
||||||
func GetsizeFull(t *os.File) (size *Winsize, err error) {
|
|
||||||
var wsz *unix.Winsize
|
|
||||||
wsz, err = unix.IoctlGetWinsize(int(t.Fd()), TIOCGWINSZ)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return &Winsize{wsz.Row, wsz.Col, wsz.Xpixel, wsz.Ypixel}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Windows Size
|
|
||||||
func Getsize(t *os.File) (rows, cols int, err error) {
|
|
||||||
var wsz *unix.Winsize
|
|
||||||
wsz, err = unix.IoctlGetWinsize(int(t.Fd()), TIOCGWINSZ)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return 80, 25, err
|
|
||||||
} else {
|
|
||||||
return int(wsz.Row), int(wsz.Col), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setsize resizes t to s.
|
|
||||||
func Setsize(t *os.File, ws *Winsize) error {
|
|
||||||
wsz := unix.Winsize{ws.Rows, ws.Cols, ws.X, ws.Y}
|
|
||||||
return unix.IoctlSetWinsize(int(t.Fd()), TIOCSWINSZ, &wsz)
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
// Created by cgo -godefs - DO NOT EDIT
|
|
||||||
// cgo -godefs types.go
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
type (
|
|
||||||
_C_int int32
|
|
||||||
_C_uint uint32
|
|
||||||
)
|
|
|
@ -1,9 +0,0 @@
|
||||||
// Created by cgo -godefs - DO NOT EDIT
|
|
||||||
// cgo -godefs types.go
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
type (
|
|
||||||
_C_int int32
|
|
||||||
_C_uint uint32
|
|
||||||
)
|
|
|
@ -1,9 +0,0 @@
|
||||||
// Created by cgo -godefs - DO NOT EDIT
|
|
||||||
// cgo -godefs types.go
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
type (
|
|
||||||
_C_int int32
|
|
||||||
_C_uint uint32
|
|
||||||
)
|
|
|
@ -1,11 +0,0 @@
|
||||||
// Created by cgo -godefs - DO NOT EDIT
|
|
||||||
// cgo -godefs types.go
|
|
||||||
|
|
||||||
// +build arm64
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
type (
|
|
||||||
_C_int int32
|
|
||||||
_C_uint uint32
|
|
||||||
)
|
|
|
@ -1,14 +0,0 @@
|
||||||
// Created by cgo -godefs - DO NOT EDIT
|
|
||||||
// cgo -godefs types_dragonfly.go
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
const (
|
|
||||||
_C_SPECNAMELEN = 0x3f
|
|
||||||
)
|
|
||||||
|
|
||||||
type fiodgnameArg struct {
|
|
||||||
Name *byte
|
|
||||||
Len uint32
|
|
||||||
Pad_cgo_0 [4]byte
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
// Created by cgo -godefs - DO NOT EDIT
|
|
||||||
// cgo -godefs types_freebsd.go
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
const (
|
|
||||||
_C_SPECNAMELEN = 0x3f
|
|
||||||
)
|
|
||||||
|
|
||||||
type fiodgnameArg struct {
|
|
||||||
Len int32
|
|
||||||
Buf *byte
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
// Created by cgo -godefs - DO NOT EDIT
|
|
||||||
// cgo -godefs types_freebsd.go
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
const (
|
|
||||||
_C_SPECNAMELEN = 0x3f
|
|
||||||
)
|
|
||||||
|
|
||||||
type fiodgnameArg struct {
|
|
||||||
Len int32
|
|
||||||
Pad_cgo_0 [4]byte
|
|
||||||
Buf *byte
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
// Created by cgo -godefs - DO NOT EDIT
|
|
||||||
// cgo -godefs types_freebsd.go
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
const (
|
|
||||||
_C_SPECNAMELEN = 0x3f
|
|
||||||
)
|
|
||||||
|
|
||||||
type fiodgnameArg struct {
|
|
||||||
Len int32
|
|
||||||
Buf *byte
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
// Created by cgo -godefs - DO NOT EDIT
|
|
||||||
// cgo -godefs types.go
|
|
||||||
|
|
||||||
// +build linux
|
|
||||||
// +build mips mipsle mips64 mips64le
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
type (
|
|
||||||
_C_int int32
|
|
||||||
_C_uint uint32
|
|
||||||
)
|
|
|
@ -1,13 +0,0 @@
|
||||||
// Created by cgo -godefs - DO NOT EDIT
|
|
||||||
// cgo -godefs types_openbsd.go
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
type ptmget struct {
|
|
||||||
Cfd int32
|
|
||||||
Sfd int32
|
|
||||||
Cn [16]int8
|
|
||||||
Sn [16]int8
|
|
||||||
}
|
|
||||||
|
|
||||||
var ioctl_PTMGET = 0x40287401
|
|
|
@ -1,13 +0,0 @@
|
||||||
// Created by cgo -godefs - DO NOT EDIT
|
|
||||||
// cgo -godefs types_openbsd.go
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
type ptmget struct {
|
|
||||||
Cfd int32
|
|
||||||
Sfd int32
|
|
||||||
Cn [16]int8
|
|
||||||
Sn [16]int8
|
|
||||||
}
|
|
||||||
|
|
||||||
var ioctl_PTMGET = 0x40287401
|
|
|
@ -1,11 +0,0 @@
|
||||||
// +build ppc64
|
|
||||||
|
|
||||||
// Created by cgo -godefs - DO NOT EDIT
|
|
||||||
// cgo -godefs types.go
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
type (
|
|
||||||
_C_int int32
|
|
||||||
_C_uint uint32
|
|
||||||
)
|
|
|
@ -1,11 +0,0 @@
|
||||||
// +build ppc64le
|
|
||||||
|
|
||||||
// Created by cgo -godefs - DO NOT EDIT
|
|
||||||
// cgo -godefs types.go
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
type (
|
|
||||||
_C_int int32
|
|
||||||
_C_uint uint32
|
|
||||||
)
|
|
|
@ -1,11 +0,0 @@
|
||||||
// Code generated by cmd/cgo -godefs; DO NOT EDIT.
|
|
||||||
// cgo -godefs types.go
|
|
||||||
|
|
||||||
// +build riscv riscv64
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
type (
|
|
||||||
_C_int int32
|
|
||||||
_C_uint uint32
|
|
||||||
)
|
|
|
@ -1,11 +0,0 @@
|
||||||
// +build s390x
|
|
||||||
|
|
||||||
// Created by cgo -godefs - DO NOT EDIT
|
|
||||||
// cgo -godefs types.go
|
|
||||||
|
|
||||||
package pty
|
|
||||||
|
|
||||||
type (
|
|
||||||
_C_int int32
|
|
||||||
_C_uint uint32
|
|
||||||
)
|
|
|
@ -1,27 +0,0 @@
|
||||||
Copyright (c) 2016 Glider Labs. All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are
|
|
||||||
met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following disclaimer
|
|
||||||
in the documentation and/or other materials provided with the
|
|
||||||
distribution.
|
|
||||||
* Neither the name of Glider Labs nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
@ -1,96 +0,0 @@
|
||||||
# gliderlabs/ssh
|
|
||||||
|
|
||||||
[![GoDoc](https://godoc.org/github.com/gliderlabs/ssh?status.svg)](https://godoc.org/github.com/gliderlabs/ssh)
|
|
||||||
[![CircleCI](https://img.shields.io/circleci/project/github/gliderlabs/ssh.svg)](https://circleci.com/gh/gliderlabs/ssh)
|
|
||||||
[![Go Report Card](https://goreportcard.com/badge/github.com/gliderlabs/ssh)](https://goreportcard.com/report/github.com/gliderlabs/ssh)
|
|
||||||
[![OpenCollective](https://opencollective.com/ssh/sponsors/badge.svg)](#sponsors)
|
|
||||||
[![Slack](http://slack.gliderlabs.com/badge.svg)](http://slack.gliderlabs.com)
|
|
||||||
[![Email Updates](https://img.shields.io/badge/updates-subscribe-yellow.svg)](https://app.convertkit.com/landing_pages/243312)
|
|
||||||
|
|
||||||
> The Glider Labs SSH server package is dope. —[@bradfitz](https://twitter.com/bradfitz), Go team member
|
|
||||||
|
|
||||||
This Go package wraps the [crypto/ssh
|
|
||||||
package](https://godoc.org/golang.org/x/crypto/ssh) with a higher-level API for
|
|
||||||
building SSH servers. The goal of the API was to make it as simple as using
|
|
||||||
[net/http](https://golang.org/pkg/net/http/), so the API is very similar:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gliderlabs/ssh"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
ssh.Handle(func(s ssh.Session) {
|
|
||||||
io.WriteString(s, "Hello world\n")
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Fatal(ssh.ListenAndServe(":2222", nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
This package was built by [@progrium](https://twitter.com/progrium) after working on nearly a dozen projects at Glider Labs using SSH and collaborating with [@shazow](https://twitter.com/shazow) (known for [ssh-chat](https://github.com/shazow/ssh-chat)).
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
A bunch of great examples are in the `_examples` directory.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
[See GoDoc reference.](https://godoc.org/github.com/gliderlabs/ssh)
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Pull requests are welcome! However, since this project is very much about API
|
|
||||||
design, please submit API changes as issues to discuss before submitting PRs.
|
|
||||||
|
|
||||||
Also, you can [join our Slack](http://slack.gliderlabs.com) to discuss as well.
|
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
* Non-session channel handlers
|
|
||||||
* Cleanup callback API
|
|
||||||
* 1.0 release
|
|
||||||
* High-level client?
|
|
||||||
|
|
||||||
## Sponsors
|
|
||||||
|
|
||||||
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/ssh#sponsor)]
|
|
||||||
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/0/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/0/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/1/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/1/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/2/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/2/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/3/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/3/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/4/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/4/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/5/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/5/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/6/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/6/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/7/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/7/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/8/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/8/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/9/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/9/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/10/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/10/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/11/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/11/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/12/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/12/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/13/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/13/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/14/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/14/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/15/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/15/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/16/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/16/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/17/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/17/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/18/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/18/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/19/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/19/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/20/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/20/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/21/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/21/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/22/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/22/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/23/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/23/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/24/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/24/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/25/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/25/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/26/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/26/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/27/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/27/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/28/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/28/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/29/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/29/avatar.svg"></a>
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
BSD
|
|
|
@ -1,83 +0,0 @@
|
||||||
package ssh
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
|
||||||
"path"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
gossh "golang.org/x/crypto/ssh"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
agentRequestType = "auth-agent-req@openssh.com"
|
|
||||||
agentChannelType = "auth-agent@openssh.com"
|
|
||||||
|
|
||||||
agentTempDir = "auth-agent"
|
|
||||||
agentListenFile = "listener.sock"
|
|
||||||
)
|
|
||||||
|
|
||||||
// contextKeyAgentRequest is an internal context key for storing if the
|
|
||||||
// client requested agent forwarding
|
|
||||||
var contextKeyAgentRequest = &contextKey{"auth-agent-req"}
|
|
||||||
|
|
||||||
// SetAgentRequested sets up the session context so that AgentRequested
|
|
||||||
// returns true.
|
|
||||||
func SetAgentRequested(ctx Context) {
|
|
||||||
ctx.SetValue(contextKeyAgentRequest, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AgentRequested returns true if the client requested agent forwarding.
|
|
||||||
func AgentRequested(sess Session) bool {
|
|
||||||
return sess.Context().Value(contextKeyAgentRequest) == true
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAgentListener sets up a temporary Unix socket that can be communicated
|
|
||||||
// to the session environment and used for forwarding connections.
|
|
||||||
func NewAgentListener() (net.Listener, error) {
|
|
||||||
dir, err := ioutil.TempDir("", agentTempDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
l, err := net.Listen("unix", path.Join(dir, agentListenFile))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return l, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ForwardAgentConnections takes connections from a listener to proxy into the
|
|
||||||
// session on the OpenSSH channel for agent connections. It blocks and services
|
|
||||||
// connections until the listener stop accepting.
|
|
||||||
func ForwardAgentConnections(l net.Listener, s Session) {
|
|
||||||
sshConn := s.Context().Value(ContextKeyConn).(gossh.Conn)
|
|
||||||
for {
|
|
||||||
conn, err := l.Accept()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
go func(conn net.Conn) {
|
|
||||||
defer conn.Close()
|
|
||||||
channel, reqs, err := sshConn.OpenChannel(agentChannelType, nil)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer channel.Close()
|
|
||||||
go gossh.DiscardRequests(reqs)
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(2)
|
|
||||||
go func() {
|
|
||||||
io.Copy(conn, channel)
|
|
||||||
conn.(*net.UnixConn).CloseWrite()
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
io.Copy(channel, conn)
|
|
||||||
channel.CloseWrite()
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
wg.Wait()
|
|
||||||
}(conn)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
version: 2
|
|
||||||
jobs:
|
|
||||||
build-go-latest:
|
|
||||||
docker:
|
|
||||||
- image: golang:latest
|
|
||||||
working_directory: /go/src/github.com/gliderlabs/ssh
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- run: go get
|
|
||||||
- run: go test -v -race
|
|
||||||
|
|
||||||
build-go-1.9:
|
|
||||||
docker:
|
|
||||||
- image: golang:1.9
|
|
||||||
working_directory: /go/src/github.com/gliderlabs/ssh
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- run: go get
|
|
||||||
- run: go test -v -race
|
|
||||||
|
|
||||||
workflows:
|
|
||||||
version: 2
|
|
||||||
build:
|
|
||||||
jobs:
|
|
||||||
- build-go-latest
|
|
||||||
- build-go-1.9
|
|
|
@ -1,55 +0,0 @@
|
||||||
package ssh
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type serverConn struct {
|
|
||||||
net.Conn
|
|
||||||
|
|
||||||
idleTimeout time.Duration
|
|
||||||
maxDeadline time.Time
|
|
||||||
closeCanceler context.CancelFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *serverConn) Write(p []byte) (n int, err error) {
|
|
||||||
c.updateDeadline()
|
|
||||||
n, err = c.Conn.Write(p)
|
|
||||||
if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
|
|
||||||
c.closeCanceler()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *serverConn) Read(b []byte) (n int, err error) {
|
|
||||||
c.updateDeadline()
|
|
||||||
n, err = c.Conn.Read(b)
|
|
||||||
if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
|
|
||||||
c.closeCanceler()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *serverConn) Close() (err error) {
|
|
||||||
err = c.Conn.Close()
|
|
||||||
if c.closeCanceler != nil {
|
|
||||||
c.closeCanceler()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *serverConn) updateDeadline() {
|
|
||||||
switch {
|
|
||||||
case c.idleTimeout > 0:
|
|
||||||
idleDeadline := time.Now().Add(c.idleTimeout)
|
|
||||||
if idleDeadline.Unix() < c.maxDeadline.Unix() || c.maxDeadline.IsZero() {
|
|
||||||
c.Conn.SetDeadline(idleDeadline)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fallthrough
|
|
||||||
default:
|
|
||||||
c.Conn.SetDeadline(c.maxDeadline)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,152 +0,0 @@
|
||||||
package ssh
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/hex"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
gossh "golang.org/x/crypto/ssh"
|
|
||||||
)
|
|
||||||
|
|
||||||
// contextKey is a value for use with context.WithValue. It's used as
|
|
||||||
// a pointer so it fits in an interface{} without allocation.
|
|
||||||
type contextKey struct {
|
|
||||||
name string
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ContextKeyUser is a context key for use with Contexts in this package.
|
|
||||||
// The associated value will be of type string.
|
|
||||||
ContextKeyUser = &contextKey{"user"}
|
|
||||||
|
|
||||||
// ContextKeySessionID is a context key for use with Contexts in this package.
|
|
||||||
// The associated value will be of type string.
|
|
||||||
ContextKeySessionID = &contextKey{"session-id"}
|
|
||||||
|
|
||||||
// ContextKeyPermissions is a context key for use with Contexts in this package.
|
|
||||||
// The associated value will be of type *Permissions.
|
|
||||||
ContextKeyPermissions = &contextKey{"permissions"}
|
|
||||||
|
|
||||||
// ContextKeyClientVersion is a context key for use with Contexts in this package.
|
|
||||||
// The associated value will be of type string.
|
|
||||||
ContextKeyClientVersion = &contextKey{"client-version"}
|
|
||||||
|
|
||||||
// ContextKeyServerVersion is a context key for use with Contexts in this package.
|
|
||||||
// The associated value will be of type string.
|
|
||||||
ContextKeyServerVersion = &contextKey{"server-version"}
|
|
||||||
|
|
||||||
// ContextKeyLocalAddr is a context key for use with Contexts in this package.
|
|
||||||
// The associated value will be of type net.Addr.
|
|
||||||
ContextKeyLocalAddr = &contextKey{"local-addr"}
|
|
||||||
|
|
||||||
// ContextKeyRemoteAddr is a context key for use with Contexts in this package.
|
|
||||||
// The associated value will be of type net.Addr.
|
|
||||||
ContextKeyRemoteAddr = &contextKey{"remote-addr"}
|
|
||||||
|
|
||||||
// ContextKeyServer is a context key for use with Contexts in this package.
|
|
||||||
// The associated value will be of type *Server.
|
|
||||||
ContextKeyServer = &contextKey{"ssh-server"}
|
|
||||||
|
|
||||||
// ContextKeyConn is a context key for use with Contexts in this package.
|
|
||||||
// The associated value will be of type gossh.ServerConn.
|
|
||||||
ContextKeyConn = &contextKey{"ssh-conn"}
|
|
||||||
|
|
||||||
// ContextKeyPublicKey is a context key for use with Contexts in this package.
|
|
||||||
// The associated value will be of type PublicKey.
|
|
||||||
ContextKeyPublicKey = &contextKey{"public-key"}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Context is a package specific context interface. It exposes connection
|
|
||||||
// metadata and allows new values to be easily written to it. It's used in
|
|
||||||
// authentication handlers and callbacks, and its underlying context.Context is
|
|
||||||
// exposed on Session in the session Handler. A connection-scoped lock is also
|
|
||||||
// embedded in the context to make it easier to limit operations per-connection.
|
|
||||||
type Context interface {
|
|
||||||
context.Context
|
|
||||||
sync.Locker
|
|
||||||
|
|
||||||
// User returns the username used when establishing the SSH connection.
|
|
||||||
User() string
|
|
||||||
|
|
||||||
// SessionID returns the session hash.
|
|
||||||
SessionID() string
|
|
||||||
|
|
||||||
// ClientVersion returns the version reported by the client.
|
|
||||||
ClientVersion() string
|
|
||||||
|
|
||||||
// ServerVersion returns the version reported by the server.
|
|
||||||
ServerVersion() string
|
|
||||||
|
|
||||||
// RemoteAddr returns the remote address for this connection.
|
|
||||||
RemoteAddr() net.Addr
|
|
||||||
|
|
||||||
// LocalAddr returns the local address for this connection.
|
|
||||||
LocalAddr() net.Addr
|
|
||||||
|
|
||||||
// Permissions returns the Permissions object used for this connection.
|
|
||||||
Permissions() *Permissions
|
|
||||||
|
|
||||||
// SetValue allows you to easily write new values into the underlying context.
|
|
||||||
SetValue(key, value interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
type sshContext struct {
|
|
||||||
context.Context
|
|
||||||
*sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func newContext(srv *Server) (*sshContext, context.CancelFunc) {
|
|
||||||
innerCtx, cancel := context.WithCancel(context.Background())
|
|
||||||
ctx := &sshContext{innerCtx, &sync.Mutex{}}
|
|
||||||
ctx.SetValue(ContextKeyServer, srv)
|
|
||||||
perms := &Permissions{&gossh.Permissions{}}
|
|
||||||
ctx.SetValue(ContextKeyPermissions, perms)
|
|
||||||
return ctx, cancel
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is separate from newContext because we will get ConnMetadata
|
|
||||||
// at different points so it needs to be applied separately
|
|
||||||
func applyConnMetadata(ctx Context, conn gossh.ConnMetadata) {
|
|
||||||
if ctx.Value(ContextKeySessionID) != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.SetValue(ContextKeySessionID, hex.EncodeToString(conn.SessionID()))
|
|
||||||
ctx.SetValue(ContextKeyClientVersion, string(conn.ClientVersion()))
|
|
||||||
ctx.SetValue(ContextKeyServerVersion, string(conn.ServerVersion()))
|
|
||||||
ctx.SetValue(ContextKeyUser, conn.User())
|
|
||||||
ctx.SetValue(ContextKeyLocalAddr, conn.LocalAddr())
|
|
||||||
ctx.SetValue(ContextKeyRemoteAddr, conn.RemoteAddr())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *sshContext) SetValue(key, value interface{}) {
|
|
||||||
ctx.Context = context.WithValue(ctx.Context, key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *sshContext) User() string {
|
|
||||||
return ctx.Value(ContextKeyUser).(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *sshContext) SessionID() string {
|
|
||||||
return ctx.Value(ContextKeySessionID).(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *sshContext) ClientVersion() string {
|
|
||||||
return ctx.Value(ContextKeyClientVersion).(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *sshContext) ServerVersion() string {
|
|
||||||
return ctx.Value(ContextKeyServerVersion).(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *sshContext) RemoteAddr() net.Addr {
|
|
||||||
return ctx.Value(ContextKeyRemoteAddr).(net.Addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *sshContext) LocalAddr() net.Addr {
|
|
||||||
return ctx.Value(ContextKeyLocalAddr).(net.Addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *sshContext) Permissions() *Permissions {
|
|
||||||
return ctx.Value(ContextKeyPermissions).(*Permissions)
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
/*
|
|
||||||
Package ssh wraps the crypto/ssh package with a higher-level API for building
|
|
||||||
SSH servers. The goal of the API was to make it as simple as using net/http, so
|
|
||||||
the API is very similar.
|
|
||||||
|
|
||||||
You should be able to build any SSH server using only this package, which wraps
|
|
||||||
relevant types and some functions from crypto/ssh. However, you still need to
|
|
||||||
use crypto/ssh for building SSH clients.
|
|
||||||
|
|
||||||
ListenAndServe starts an SSH server with a given address, handler, and options. The
|
|
||||||
handler is usually nil, which means to use DefaultHandler. Handle sets DefaultHandler:
|
|
||||||
|
|
||||||
ssh.Handle(func(s ssh.Session) {
|
|
||||||
io.WriteString(s, "Hello world\n")
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Fatal(ssh.ListenAndServe(":2222", nil))
|
|
||||||
|
|
||||||
If you don't specify a host key, it will generate one every time. This is convenient
|
|
||||||
except you'll have to deal with clients being confused that the host key is different.
|
|
||||||
It's a better idea to generate or point to an existing key on your system:
|
|
||||||
|
|
||||||
log.Fatal(ssh.ListenAndServe(":2222", nil, ssh.HostKeyFile("/Users/progrium/.ssh/id_rsa")))
|
|
||||||
|
|
||||||
Although all options have functional option helpers, another way to control the
|
|
||||||
server's behavior is by creating a custom Server:
|
|
||||||
|
|
||||||
s := &ssh.Server{
|
|
||||||
Addr: ":2222",
|
|
||||||
Handler: sessionHandler,
|
|
||||||
PublicKeyHandler: authHandler,
|
|
||||||
}
|
|
||||||
s.AddHostKey(hostKeySigner)
|
|
||||||
|
|
||||||
log.Fatal(s.ListenAndServe())
|
|
||||||
|
|
||||||
This package automatically handles basic SSH requests like setting environment
|
|
||||||
variables, requesting PTY, and changing window size. These requests are
|
|
||||||
processed, responded to, and any relevant state is updated. This state is then
|
|
||||||
exposed to you via the Session interface.
|
|
||||||
|
|
||||||
The one big feature missing from the Session abstraction is signals. This was
|
|
||||||
started, but not completed. Pull Requests welcome!
|
|
||||||
*/
|
|
||||||
package ssh
|
|
|
@ -1,77 +0,0 @@
|
||||||
package ssh
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
|
|
||||||
gossh "golang.org/x/crypto/ssh"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PasswordAuth returns a functional option that sets PasswordHandler on the server.
|
|
||||||
func PasswordAuth(fn PasswordHandler) Option {
|
|
||||||
return func(srv *Server) error {
|
|
||||||
srv.PasswordHandler = fn
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PublicKeyAuth returns a functional option that sets PublicKeyHandler on the server.
|
|
||||||
func PublicKeyAuth(fn PublicKeyHandler) Option {
|
|
||||||
return func(srv *Server) error {
|
|
||||||
srv.PublicKeyHandler = fn
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HostKeyFile returns a functional option that adds HostSigners to the server
|
|
||||||
// from a PEM file at filepath.
|
|
||||||
func HostKeyFile(filepath string) Option {
|
|
||||||
return func(srv *Server) error {
|
|
||||||
pemBytes, err := ioutil.ReadFile(filepath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
signer, err := gossh.ParsePrivateKey(pemBytes)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
srv.AddHostKey(signer)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HostKeyPEM returns a functional option that adds HostSigners to the server
|
|
||||||
// from a PEM file as bytes.
|
|
||||||
func HostKeyPEM(bytes []byte) Option {
|
|
||||||
return func(srv *Server) error {
|
|
||||||
signer, err := gossh.ParsePrivateKey(bytes)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
srv.AddHostKey(signer)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoPty returns a functional option that sets PtyCallback to return false,
|
|
||||||
// denying PTY requests.
|
|
||||||
func NoPty() Option {
|
|
||||||
return func(srv *Server) error {
|
|
||||||
srv.PtyCallback = func(ctx Context, pty Pty) bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WrapConn returns a functional option that sets ConnCallback on the server.
|
|
||||||
func WrapConn(fn ConnCallback) Option {
|
|
||||||
return func(srv *Server) error {
|
|
||||||
srv.ConnCallback = fn
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,394 +0,0 @@
|
||||||
package ssh
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
gossh "golang.org/x/crypto/ssh"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrServerClosed is returned by the Server's Serve, ListenAndServe,
|
|
||||||
// and ListenAndServeTLS methods after a call to Shutdown or Close.
|
|
||||||
var ErrServerClosed = errors.New("ssh: Server closed")
|
|
||||||
|
|
||||||
type RequestHandler func(ctx Context, srv *Server, req *gossh.Request) (ok bool, payload []byte)
|
|
||||||
|
|
||||||
var DefaultRequestHandlers = map[string]RequestHandler{}
|
|
||||||
|
|
||||||
type ChannelHandler func(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context)
|
|
||||||
|
|
||||||
var DefaultChannelHandlers = map[string]ChannelHandler{
|
|
||||||
"session": DefaultSessionHandler,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server defines parameters for running an SSH server. The zero value for
|
|
||||||
// Server is a valid configuration. When both PasswordHandler and
|
|
||||||
// PublicKeyHandler are nil, no client authentication is performed.
|
|
||||||
type Server struct {
|
|
||||||
Addr string // TCP address to listen on, ":22" if empty
|
|
||||||
Handler Handler // handler to invoke, ssh.DefaultHandler if nil
|
|
||||||
HostSigners []Signer // private keys for the host key, must have at least one
|
|
||||||
Version string // server version to be sent before the initial handshake
|
|
||||||
|
|
||||||
KeyboardInteractiveHandler KeyboardInteractiveHandler // keyboard-interactive authentication handler
|
|
||||||
PasswordHandler PasswordHandler // password authentication handler
|
|
||||||
PublicKeyHandler PublicKeyHandler // public key authentication handler
|
|
||||||
PtyCallback PtyCallback // callback for allowing PTY sessions, allows all if nil
|
|
||||||
ConnCallback ConnCallback // optional callback for wrapping net.Conn before handling
|
|
||||||
LocalPortForwardingCallback LocalPortForwardingCallback // callback for allowing local port forwarding, denies all if nil
|
|
||||||
ReversePortForwardingCallback ReversePortForwardingCallback // callback for allowing reverse port forwarding, denies all if nil
|
|
||||||
ServerConfigCallback ServerConfigCallback // callback for configuring detailed SSH options
|
|
||||||
SessionRequestCallback SessionRequestCallback // callback for allowing or denying SSH sessions
|
|
||||||
|
|
||||||
IdleTimeout time.Duration // connection timeout when no activity, none if empty
|
|
||||||
MaxTimeout time.Duration // absolute connection timeout, none if empty
|
|
||||||
|
|
||||||
// ChannelHandlers allow overriding the built-in session handlers or provide
|
|
||||||
// extensions to the protocol, such as tcpip forwarding. By default only the
|
|
||||||
// "session" handler is enabled.
|
|
||||||
ChannelHandlers map[string]ChannelHandler
|
|
||||||
|
|
||||||
// RequestHandlers allow overriding the server-level request handlers or
|
|
||||||
// provide extensions to the protocol, such as tcpip forwarding. By default
|
|
||||||
// no handlers are enabled.
|
|
||||||
RequestHandlers map[string]RequestHandler
|
|
||||||
|
|
||||||
listenerWg sync.WaitGroup
|
|
||||||
mu sync.Mutex
|
|
||||||
listeners map[net.Listener]struct{}
|
|
||||||
conns map[*gossh.ServerConn]struct{}
|
|
||||||
connWg sync.WaitGroup
|
|
||||||
doneChan chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *Server) ensureHostSigner() error {
|
|
||||||
if len(srv.HostSigners) == 0 {
|
|
||||||
signer, err := generateSigner()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
srv.HostSigners = append(srv.HostSigners, signer)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *Server) ensureHandlers() {
|
|
||||||
srv.mu.Lock()
|
|
||||||
defer srv.mu.Unlock()
|
|
||||||
if srv.RequestHandlers == nil {
|
|
||||||
srv.RequestHandlers = map[string]RequestHandler{}
|
|
||||||
for k, v := range DefaultRequestHandlers {
|
|
||||||
srv.RequestHandlers[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if srv.ChannelHandlers == nil {
|
|
||||||
srv.ChannelHandlers = map[string]ChannelHandler{}
|
|
||||||
for k, v := range DefaultChannelHandlers {
|
|
||||||
srv.ChannelHandlers[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *Server) config(ctx Context) *gossh.ServerConfig {
|
|
||||||
var config *gossh.ServerConfig
|
|
||||||
if srv.ServerConfigCallback == nil {
|
|
||||||
config = &gossh.ServerConfig{}
|
|
||||||
} else {
|
|
||||||
config = srv.ServerConfigCallback(ctx)
|
|
||||||
}
|
|
||||||
for _, signer := range srv.HostSigners {
|
|
||||||
config.AddHostKey(signer)
|
|
||||||
}
|
|
||||||
if srv.PasswordHandler == nil && srv.PublicKeyHandler == nil {
|
|
||||||
config.NoClientAuth = true
|
|
||||||
}
|
|
||||||
if srv.Version != "" {
|
|
||||||
config.ServerVersion = "SSH-2.0-" + srv.Version
|
|
||||||
}
|
|
||||||
if srv.PasswordHandler != nil {
|
|
||||||
config.PasswordCallback = func(conn gossh.ConnMetadata, password []byte) (*gossh.Permissions, error) {
|
|
||||||
applyConnMetadata(ctx, conn)
|
|
||||||
if ok := srv.PasswordHandler(ctx, string(password)); !ok {
|
|
||||||
return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
|
|
||||||
}
|
|
||||||
return ctx.Permissions().Permissions, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if srv.PublicKeyHandler != nil {
|
|
||||||
config.PublicKeyCallback = func(conn gossh.ConnMetadata, key gossh.PublicKey) (*gossh.Permissions, error) {
|
|
||||||
applyConnMetadata(ctx, conn)
|
|
||||||
if ok := srv.PublicKeyHandler(ctx, key); !ok {
|
|
||||||
return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
|
|
||||||
}
|
|
||||||
ctx.SetValue(ContextKeyPublicKey, key)
|
|
||||||
return ctx.Permissions().Permissions, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if srv.KeyboardInteractiveHandler != nil {
|
|
||||||
config.KeyboardInteractiveCallback = func(conn gossh.ConnMetadata, challenger gossh.KeyboardInteractiveChallenge) (*gossh.Permissions, error) {
|
|
||||||
if ok := srv.KeyboardInteractiveHandler(ctx, challenger); !ok {
|
|
||||||
return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
|
|
||||||
}
|
|
||||||
return ctx.Permissions().Permissions, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle sets the Handler for the server.
|
|
||||||
func (srv *Server) Handle(fn Handler) {
|
|
||||||
srv.Handler = fn
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close immediately closes all active listeners and all active
|
|
||||||
// connections.
|
|
||||||
//
|
|
||||||
// Close returns any error returned from closing the Server's
|
|
||||||
// underlying Listener(s).
|
|
||||||
func (srv *Server) Close() error {
|
|
||||||
srv.mu.Lock()
|
|
||||||
defer srv.mu.Unlock()
|
|
||||||
srv.closeDoneChanLocked()
|
|
||||||
err := srv.closeListenersLocked()
|
|
||||||
for c := range srv.conns {
|
|
||||||
c.Close()
|
|
||||||
delete(srv.conns, c)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown gracefully shuts down the server without interrupting any
|
|
||||||
// active connections. Shutdown works by first closing all open
|
|
||||||
// listeners, and then waiting indefinitely for connections to close.
|
|
||||||
// If the provided context expires before the shutdown is complete,
|
|
||||||
// then the context's error is returned.
|
|
||||||
func (srv *Server) Shutdown(ctx context.Context) error {
|
|
||||||
srv.mu.Lock()
|
|
||||||
lnerr := srv.closeListenersLocked()
|
|
||||||
srv.closeDoneChanLocked()
|
|
||||||
srv.mu.Unlock()
|
|
||||||
|
|
||||||
finished := make(chan struct{}, 1)
|
|
||||||
go func() {
|
|
||||||
srv.listenerWg.Wait()
|
|
||||||
srv.connWg.Wait()
|
|
||||||
finished <- struct{}{}
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-finished:
|
|
||||||
return lnerr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serve accepts incoming connections on the Listener l, creating a new
|
|
||||||
// connection goroutine for each. The connection goroutines read requests and then
|
|
||||||
// calls srv.Handler to handle sessions.
|
|
||||||
//
|
|
||||||
// Serve always returns a non-nil error.
|
|
||||||
func (srv *Server) Serve(l net.Listener) error {
|
|
||||||
srv.ensureHandlers()
|
|
||||||
defer l.Close()
|
|
||||||
if err := srv.ensureHostSigner(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if srv.Handler == nil {
|
|
||||||
srv.Handler = DefaultHandler
|
|
||||||
}
|
|
||||||
var tempDelay time.Duration
|
|
||||||
|
|
||||||
srv.trackListener(l, true)
|
|
||||||
defer srv.trackListener(l, false)
|
|
||||||
for {
|
|
||||||
conn, e := l.Accept()
|
|
||||||
if e != nil {
|
|
||||||
select {
|
|
||||||
case <-srv.getDoneChan():
|
|
||||||
return ErrServerClosed
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
if ne, ok := e.(net.Error); ok && ne.Temporary() {
|
|
||||||
if tempDelay == 0 {
|
|
||||||
tempDelay = 5 * time.Millisecond
|
|
||||||
} else {
|
|
||||||
tempDelay *= 2
|
|
||||||
}
|
|
||||||
if max := 1 * time.Second; tempDelay > max {
|
|
||||||
tempDelay = max
|
|
||||||
}
|
|
||||||
time.Sleep(tempDelay)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
go srv.handleConn(conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *Server) handleConn(newConn net.Conn) {
|
|
||||||
if srv.ConnCallback != nil {
|
|
||||||
cbConn := srv.ConnCallback(newConn)
|
|
||||||
if cbConn == nil {
|
|
||||||
newConn.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
newConn = cbConn
|
|
||||||
}
|
|
||||||
ctx, cancel := newContext(srv)
|
|
||||||
conn := &serverConn{
|
|
||||||
Conn: newConn,
|
|
||||||
idleTimeout: srv.IdleTimeout,
|
|
||||||
closeCanceler: cancel,
|
|
||||||
}
|
|
||||||
if srv.MaxTimeout > 0 {
|
|
||||||
conn.maxDeadline = time.Now().Add(srv.MaxTimeout)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
sshConn, chans, reqs, err := gossh.NewServerConn(conn, srv.config(ctx))
|
|
||||||
if err != nil {
|
|
||||||
// TODO: trigger event callback
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
srv.trackConn(sshConn, true)
|
|
||||||
defer srv.trackConn(sshConn, false)
|
|
||||||
|
|
||||||
ctx.SetValue(ContextKeyConn, sshConn)
|
|
||||||
applyConnMetadata(ctx, sshConn)
|
|
||||||
//go gossh.DiscardRequests(reqs)
|
|
||||||
go srv.handleRequests(ctx, reqs)
|
|
||||||
for ch := range chans {
|
|
||||||
handler := srv.ChannelHandlers[ch.ChannelType()]
|
|
||||||
if handler == nil {
|
|
||||||
handler = srv.ChannelHandlers["default"]
|
|
||||||
}
|
|
||||||
if handler == nil {
|
|
||||||
ch.Reject(gossh.UnknownChannelType, "unsupported channel type")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
go handler(srv, sshConn, ch, ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *Server) handleRequests(ctx Context, in <-chan *gossh.Request) {
|
|
||||||
for req := range in {
|
|
||||||
handler := srv.RequestHandlers[req.Type]
|
|
||||||
if handler == nil {
|
|
||||||
handler = srv.RequestHandlers["default"]
|
|
||||||
}
|
|
||||||
if handler == nil {
|
|
||||||
req.Reply(false, nil)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
/*reqCtx, cancel := context.WithCancel(ctx)
|
|
||||||
defer cancel() */
|
|
||||||
ret, payload := handler(ctx, srv, req)
|
|
||||||
req.Reply(ret, payload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListenAndServe listens on the TCP network address srv.Addr and then calls
|
|
||||||
// Serve to handle incoming connections. If srv.Addr is blank, ":22" is used.
|
|
||||||
// ListenAndServe always returns a non-nil error.
|
|
||||||
func (srv *Server) ListenAndServe() error {
|
|
||||||
addr := srv.Addr
|
|
||||||
if addr == "" {
|
|
||||||
addr = ":22"
|
|
||||||
}
|
|
||||||
ln, err := net.Listen("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return srv.Serve(ln)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddHostKey adds a private key as a host key. If an existing host key exists
|
|
||||||
// with the same algorithm, it is overwritten. Each server config must have at
|
|
||||||
// least one host key.
|
|
||||||
func (srv *Server) AddHostKey(key Signer) {
|
|
||||||
// these are later added via AddHostKey on ServerConfig, which performs the
|
|
||||||
// check for one of every algorithm.
|
|
||||||
srv.HostSigners = append(srv.HostSigners, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetOption runs a functional option against the server.
|
|
||||||
func (srv *Server) SetOption(option Option) error {
|
|
||||||
return option(srv)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *Server) getDoneChan() <-chan struct{} {
|
|
||||||
srv.mu.Lock()
|
|
||||||
defer srv.mu.Unlock()
|
|
||||||
return srv.getDoneChanLocked()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *Server) getDoneChanLocked() chan struct{} {
|
|
||||||
if srv.doneChan == nil {
|
|
||||||
srv.doneChan = make(chan struct{})
|
|
||||||
}
|
|
||||||
return srv.doneChan
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *Server) closeDoneChanLocked() {
|
|
||||||
ch := srv.getDoneChanLocked()
|
|
||||||
select {
|
|
||||||
case <-ch:
|
|
||||||
// Already closed. Don't close again.
|
|
||||||
default:
|
|
||||||
// Safe to close here. We're the only closer, guarded
|
|
||||||
// by srv.mu.
|
|
||||||
close(ch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *Server) closeListenersLocked() error {
|
|
||||||
var err error
|
|
||||||
for ln := range srv.listeners {
|
|
||||||
if cerr := ln.Close(); cerr != nil && err == nil {
|
|
||||||
err = cerr
|
|
||||||
}
|
|
||||||
delete(srv.listeners, ln)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *Server) trackListener(ln net.Listener, add bool) {
|
|
||||||
srv.mu.Lock()
|
|
||||||
defer srv.mu.Unlock()
|
|
||||||
if srv.listeners == nil {
|
|
||||||
srv.listeners = make(map[net.Listener]struct{})
|
|
||||||
}
|
|
||||||
if add {
|
|
||||||
// If the *Server is being reused after a previous
|
|
||||||
// Close or Shutdown, reset its doneChan:
|
|
||||||
if len(srv.listeners) == 0 && len(srv.conns) == 0 {
|
|
||||||
srv.doneChan = nil
|
|
||||||
}
|
|
||||||
srv.listeners[ln] = struct{}{}
|
|
||||||
srv.listenerWg.Add(1)
|
|
||||||
} else {
|
|
||||||
delete(srv.listeners, ln)
|
|
||||||
srv.listenerWg.Done()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *Server) trackConn(c *gossh.ServerConn, add bool) {
|
|
||||||
srv.mu.Lock()
|
|
||||||
defer srv.mu.Unlock()
|
|
||||||
if srv.conns == nil {
|
|
||||||
srv.conns = make(map[*gossh.ServerConn]struct{})
|
|
||||||
}
|
|
||||||
if add {
|
|
||||||
srv.conns[c] = struct{}{}
|
|
||||||
srv.connWg.Add(1)
|
|
||||||
} else {
|
|
||||||
delete(srv.conns, c)
|
|
||||||
srv.connWg.Done()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,308 +0,0 @@
|
||||||
package ssh
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/anmitsu/go-shlex"
|
|
||||||
gossh "golang.org/x/crypto/ssh"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Session provides access to information about an SSH session and methods
|
|
||||||
// to read and write to the SSH channel with an embedded Channel interface from
|
|
||||||
// cypto/ssh.
|
|
||||||
//
|
|
||||||
// When Command() returns an empty slice, the user requested a shell. Otherwise
|
|
||||||
// the user is performing an exec with those command arguments.
|
|
||||||
//
|
|
||||||
// TODO: Signals
|
|
||||||
type Session interface {
|
|
||||||
gossh.Channel
|
|
||||||
|
|
||||||
// User returns the username used when establishing the SSH connection.
|
|
||||||
User() string
|
|
||||||
|
|
||||||
// RemoteAddr returns the net.Addr of the client side of the connection.
|
|
||||||
RemoteAddr() net.Addr
|
|
||||||
|
|
||||||
// LocalAddr returns the net.Addr of the server side of the connection.
|
|
||||||
LocalAddr() net.Addr
|
|
||||||
|
|
||||||
// Environ returns a copy of strings representing the environment set by the
|
|
||||||
// user for this session, in the form "key=value".
|
|
||||||
Environ() []string
|
|
||||||
|
|
||||||
// Exit sends an exit status and then closes the session.
|
|
||||||
Exit(code int) error
|
|
||||||
|
|
||||||
// Command returns a shell parsed slice of arguments that were provided by the
|
|
||||||
// user. Shell parsing splits the command string according to POSIX shell rules,
|
|
||||||
// which considers quoting not just whitespace.
|
|
||||||
Command() []string
|
|
||||||
|
|
||||||
// RawCommand returns the exact command that was provided by the user.
|
|
||||||
RawCommand() string
|
|
||||||
|
|
||||||
// PublicKey returns the PublicKey used to authenticate. If a public key was not
|
|
||||||
// used it will return nil.
|
|
||||||
PublicKey() PublicKey
|
|
||||||
|
|
||||||
// Context returns the connection's context. The returned context is always
|
|
||||||
// non-nil and holds the same data as the Context passed into auth
|
|
||||||
// handlers and callbacks.
|
|
||||||
//
|
|
||||||
// The context is canceled when the client's connection closes or I/O
|
|
||||||
// operation fails.
|
|
||||||
Context() context.Context
|
|
||||||
|
|
||||||
// Permissions returns a copy of the Permissions object that was available for
|
|
||||||
// setup in the auth handlers via the Context.
|
|
||||||
Permissions() Permissions
|
|
||||||
|
|
||||||
// Pty returns PTY information, a channel of window size changes, and a boolean
|
|
||||||
// of whether or not a PTY was accepted for this session.
|
|
||||||
Pty() (Pty, <-chan Window, bool)
|
|
||||||
|
|
||||||
// Signals registers a channel to receive signals sent from the client. The
|
|
||||||
// channel must handle signal sends or it will block the SSH request loop.
|
|
||||||
// Registering nil will unregister the channel from signal sends. During the
|
|
||||||
// time no channel is registered signals are buffered up to a reasonable amount.
|
|
||||||
// If there are buffered signals when a channel is registered, they will be
|
|
||||||
// sent in order on the channel immediately after registering.
|
|
||||||
Signals(c chan<- Signal)
|
|
||||||
}
|
|
||||||
|
|
||||||
// maxSigBufSize is how many signals will be buffered
|
|
||||||
// when there is no signal channel specified
|
|
||||||
const maxSigBufSize = 128
|
|
||||||
|
|
||||||
func DefaultSessionHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) {
|
|
||||||
ch, reqs, err := newChan.Accept()
|
|
||||||
if err != nil {
|
|
||||||
// TODO: trigger event callback
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sess := &session{
|
|
||||||
Channel: ch,
|
|
||||||
conn: conn,
|
|
||||||
handler: srv.Handler,
|
|
||||||
ptyCb: srv.PtyCallback,
|
|
||||||
sessReqCb: srv.SessionRequestCallback,
|
|
||||||
ctx: ctx,
|
|
||||||
}
|
|
||||||
sess.handleRequests(reqs)
|
|
||||||
}
|
|
||||||
|
|
||||||
type session struct {
|
|
||||||
sync.Mutex
|
|
||||||
gossh.Channel
|
|
||||||
conn *gossh.ServerConn
|
|
||||||
handler Handler
|
|
||||||
handled bool
|
|
||||||
exited bool
|
|
||||||
pty *Pty
|
|
||||||
winch chan Window
|
|
||||||
env []string
|
|
||||||
ptyCb PtyCallback
|
|
||||||
sessReqCb SessionRequestCallback
|
|
||||||
rawCmd string
|
|
||||||
ctx Context
|
|
||||||
sigCh chan<- Signal
|
|
||||||
sigBuf []Signal
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sess *session) Write(p []byte) (n int, err error) {
|
|
||||||
if sess.pty != nil {
|
|
||||||
m := len(p)
|
|
||||||
// normalize \n to \r\n when pty is accepted.
|
|
||||||
// this is a hardcoded shortcut since we don't support terminal modes.
|
|
||||||
p = bytes.Replace(p, []byte{'\n'}, []byte{'\r', '\n'}, -1)
|
|
||||||
p = bytes.Replace(p, []byte{'\r', '\r', '\n'}, []byte{'\r', '\n'}, -1)
|
|
||||||
n, err = sess.Channel.Write(p)
|
|
||||||
if n > m {
|
|
||||||
n = m
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return sess.Channel.Write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sess *session) PublicKey() PublicKey {
|
|
||||||
sessionkey := sess.ctx.Value(ContextKeyPublicKey)
|
|
||||||
if sessionkey == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return sessionkey.(PublicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sess *session) Permissions() Permissions {
|
|
||||||
// use context permissions because its properly
|
|
||||||
// wrapped and easier to dereference
|
|
||||||
perms := sess.ctx.Value(ContextKeyPermissions).(*Permissions)
|
|
||||||
return *perms
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sess *session) Context() context.Context {
|
|
||||||
return sess.ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sess *session) Exit(code int) error {
|
|
||||||
sess.Lock()
|
|
||||||
defer sess.Unlock()
|
|
||||||
if sess.exited {
|
|
||||||
return errors.New("Session.Exit called multiple times")
|
|
||||||
}
|
|
||||||
sess.exited = true
|
|
||||||
|
|
||||||
status := struct{ Status uint32 }{uint32(code)}
|
|
||||||
_, err := sess.SendRequest("exit-status", false, gossh.Marshal(&status))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return sess.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sess *session) User() string {
|
|
||||||
return sess.conn.User()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sess *session) RemoteAddr() net.Addr {
|
|
||||||
return sess.conn.RemoteAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sess *session) LocalAddr() net.Addr {
|
|
||||||
return sess.conn.LocalAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sess *session) Environ() []string {
|
|
||||||
return append([]string(nil), sess.env...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sess *session) RawCommand() string {
|
|
||||||
return sess.rawCmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sess *session) Command() []string {
|
|
||||||
cmd, _ := shlex.Split(sess.rawCmd, true)
|
|
||||||
return append([]string(nil), cmd...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sess *session) Pty() (Pty, <-chan Window, bool) {
|
|
||||||
if sess.pty != nil {
|
|
||||||
return *sess.pty, sess.winch, true
|
|
||||||
}
|
|
||||||
return Pty{}, sess.winch, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sess *session) Signals(c chan<- Signal) {
|
|
||||||
sess.Lock()
|
|
||||||
defer sess.Unlock()
|
|
||||||
sess.sigCh = c
|
|
||||||
if len(sess.sigBuf) > 0 {
|
|
||||||
go func() {
|
|
||||||
for _, sig := range sess.sigBuf {
|
|
||||||
sess.sigCh <- sig
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
|
|
||||||
for req := range reqs {
|
|
||||||
switch req.Type {
|
|
||||||
case "shell", "exec":
|
|
||||||
if sess.handled {
|
|
||||||
req.Reply(false, nil)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload = struct{ Value string }{}
|
|
||||||
gossh.Unmarshal(req.Payload, &payload)
|
|
||||||
sess.rawCmd = payload.Value
|
|
||||||
|
|
||||||
// If there's a session policy callback, we need to confirm before
|
|
||||||
// accepting the session.
|
|
||||||
if sess.sessReqCb != nil && !sess.sessReqCb(sess, req.Type) {
|
|
||||||
sess.rawCmd = ""
|
|
||||||
req.Reply(false, nil)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
sess.handled = true
|
|
||||||
req.Reply(true, nil)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
sess.handler(sess)
|
|
||||||
sess.Exit(0)
|
|
||||||
}()
|
|
||||||
case "env":
|
|
||||||
if sess.handled {
|
|
||||||
req.Reply(false, nil)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var kv struct{ Key, Value string }
|
|
||||||
gossh.Unmarshal(req.Payload, &kv)
|
|
||||||
sess.env = append(sess.env, fmt.Sprintf("%s=%s", kv.Key, kv.Value))
|
|
||||||
req.Reply(true, nil)
|
|
||||||
case "signal":
|
|
||||||
var payload struct{ Signal string }
|
|
||||||
gossh.Unmarshal(req.Payload, &payload)
|
|
||||||
sess.Lock()
|
|
||||||
if sess.sigCh != nil {
|
|
||||||
sess.sigCh <- Signal(payload.Signal)
|
|
||||||
} else {
|
|
||||||
if len(sess.sigBuf) < maxSigBufSize {
|
|
||||||
sess.sigBuf = append(sess.sigBuf, Signal(payload.Signal))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sess.Unlock()
|
|
||||||
case "pty-req":
|
|
||||||
if sess.handled || sess.pty != nil {
|
|
||||||
req.Reply(false, nil)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ptyReq, ok := parsePtyRequest(req.Payload)
|
|
||||||
if !ok {
|
|
||||||
req.Reply(false, nil)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if sess.ptyCb != nil {
|
|
||||||
ok := sess.ptyCb(sess.ctx, ptyReq)
|
|
||||||
if !ok {
|
|
||||||
req.Reply(false, nil)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sess.pty = &ptyReq
|
|
||||||
sess.winch = make(chan Window, 1)
|
|
||||||
sess.winch <- ptyReq.Window
|
|
||||||
defer func() {
|
|
||||||
// when reqs is closed
|
|
||||||
close(sess.winch)
|
|
||||||
}()
|
|
||||||
req.Reply(ok, nil)
|
|
||||||
case "window-change":
|
|
||||||
if sess.pty == nil {
|
|
||||||
req.Reply(false, nil)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
win, ok := parseWinchRequest(req.Payload)
|
|
||||||
if ok {
|
|
||||||
sess.pty.Window = win
|
|
||||||
sess.winch <- win
|
|
||||||
}
|
|
||||||
req.Reply(ok, nil)
|
|
||||||
case agentRequestType:
|
|
||||||
// TODO: option/callback to allow agent forwarding
|
|
||||||
SetAgentRequested(sess.ctx)
|
|
||||||
req.Reply(true, nil)
|
|
||||||
default:
|
|
||||||
// TODO: debug log
|
|
||||||
req.Reply(false, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
package ssh
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/subtle"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
gossh "golang.org/x/crypto/ssh"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Signal string
|
|
||||||
|
|
||||||
// POSIX signals as listed in RFC 4254 Section 6.10.
|
|
||||||
const (
|
|
||||||
SIGABRT Signal = "ABRT"
|
|
||||||
SIGALRM Signal = "ALRM"
|
|
||||||
SIGFPE Signal = "FPE"
|
|
||||||
SIGHUP Signal = "HUP"
|
|
||||||
SIGILL Signal = "ILL"
|
|
||||||
SIGINT Signal = "INT"
|
|
||||||
SIGKILL Signal = "KILL"
|
|
||||||
SIGPIPE Signal = "PIPE"
|
|
||||||
SIGQUIT Signal = "QUIT"
|
|
||||||
SIGSEGV Signal = "SEGV"
|
|
||||||
SIGTERM Signal = "TERM"
|
|
||||||
SIGUSR1 Signal = "USR1"
|
|
||||||
SIGUSR2 Signal = "USR2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DefaultHandler is the default Handler used by Serve.
|
|
||||||
var DefaultHandler Handler
|
|
||||||
|
|
||||||
// Option is a functional option handler for Server.
|
|
||||||
type Option func(*Server) error
|
|
||||||
|
|
||||||
// Handler is a callback for handling established SSH sessions.
|
|
||||||
type Handler func(Session)
|
|
||||||
|
|
||||||
// PublicKeyHandler is a callback for performing public key authentication.
|
|
||||||
type PublicKeyHandler func(ctx Context, key PublicKey) bool
|
|
||||||
|
|
||||||
// PasswordHandler is a callback for performing password authentication.
|
|
||||||
type PasswordHandler func(ctx Context, password string) bool
|
|
||||||
|
|
||||||
// KeyboardInteractiveHandler is a callback for performing keyboard-interactive authentication.
|
|
||||||
type KeyboardInteractiveHandler func(ctx Context, challenger gossh.KeyboardInteractiveChallenge) bool
|
|
||||||
|
|
||||||
// PtyCallback is a hook for allowing PTY sessions.
|
|
||||||
type PtyCallback func(ctx Context, pty Pty) bool
|
|
||||||
|
|
||||||
// SessionRequestCallback is a callback for allowing or denying SSH sessions.
|
|
||||||
type SessionRequestCallback func(sess Session, requestType string) bool
|
|
||||||
|
|
||||||
// ConnCallback is a hook for new connections before handling.
|
|
||||||
// It allows wrapping for timeouts and limiting by returning
|
|
||||||
// the net.Conn that will be used as the underlying connection.
|
|
||||||
type ConnCallback func(conn net.Conn) net.Conn
|
|
||||||
|
|
||||||
// LocalPortForwardingCallback is a hook for allowing port forwarding
|
|
||||||
type LocalPortForwardingCallback func(ctx Context, destinationHost string, destinationPort uint32) bool
|
|
||||||
|
|
||||||
// ReversePortForwardingCallback is a hook for allowing reverse port forwarding
|
|
||||||
type ReversePortForwardingCallback func(ctx Context, bindHost string, bindPort uint32) bool
|
|
||||||
|
|
||||||
// ServerConfigCallback is a hook for creating custom default server configs
|
|
||||||
type ServerConfigCallback func(ctx Context) *gossh.ServerConfig
|
|
||||||
|
|
||||||
// Window represents the size of a PTY window.
|
|
||||||
type Window struct {
|
|
||||||
Width int
|
|
||||||
Height int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pty represents a PTY request and configuration.
|
|
||||||
type Pty struct {
|
|
||||||
Term string
|
|
||||||
Window Window
|
|
||||||
// HELP WANTED: terminal modes!
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serve accepts incoming SSH connections on the listener l, creating a new
|
|
||||||
// connection goroutine for each. The connection goroutines read requests and
|
|
||||||
// then calls handler to handle sessions. Handler is typically nil, in which
|
|
||||||
// case the DefaultHandler is used.
|
|
||||||
func Serve(l net.Listener, handler Handler, options ...Option) error {
|
|
||||||
srv := &Server{Handler: handler}
|
|
||||||
for _, option := range options {
|
|
||||||
if err := srv.SetOption(option); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return srv.Serve(l)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListenAndServe listens on the TCP network address addr and then calls Serve
|
|
||||||
// with handler to handle sessions on incoming connections. Handler is typically
|
|
||||||
// nil, in which case the DefaultHandler is used.
|
|
||||||
func ListenAndServe(addr string, handler Handler, options ...Option) error {
|
|
||||||
srv := &Server{Addr: addr, Handler: handler}
|
|
||||||
for _, option := range options {
|
|
||||||
if err := srv.SetOption(option); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return srv.ListenAndServe()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle registers the handler as the DefaultHandler.
|
|
||||||
func Handle(handler Handler) {
|
|
||||||
DefaultHandler = handler
|
|
||||||
}
|
|
||||||
|
|
||||||
// KeysEqual is constant time compare of the keys to avoid timing attacks.
|
|
||||||
func KeysEqual(ak, bk PublicKey) bool {
|
|
||||||
|
|
||||||
//avoid panic if one of the keys is nil, return false instead
|
|
||||||
if ak == nil || bk == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
a := ak.Marshal()
|
|
||||||
b := bk.Marshal()
|
|
||||||
return (len(a) == len(b) && subtle.ConstantTimeCompare(a, b) == 1)
|
|
||||||
}
|
|
|
@ -1,193 +0,0 @@
|
||||||
package ssh
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
gossh "golang.org/x/crypto/ssh"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
forwardedTCPChannelType = "forwarded-tcpip"
|
|
||||||
)
|
|
||||||
|
|
||||||
// direct-tcpip data struct as specified in RFC4254, Section 7.2
|
|
||||||
type localForwardChannelData struct {
|
|
||||||
DestAddr string
|
|
||||||
DestPort uint32
|
|
||||||
|
|
||||||
OriginAddr string
|
|
||||||
OriginPort uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// DirectTCPIPHandler can be enabled by adding it to the server's
|
|
||||||
// ChannelHandlers under direct-tcpip.
|
|
||||||
func DirectTCPIPHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) {
|
|
||||||
d := localForwardChannelData{}
|
|
||||||
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
|
|
||||||
newChan.Reject(gossh.ConnectionFailed, "error parsing forward data: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if srv.LocalPortForwardingCallback == nil || !srv.LocalPortForwardingCallback(ctx, d.DestAddr, d.DestPort) {
|
|
||||||
newChan.Reject(gossh.Prohibited, "port forwarding is disabled")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dest := net.JoinHostPort(d.DestAddr, strconv.FormatInt(int64(d.DestPort), 10))
|
|
||||||
|
|
||||||
var dialer net.Dialer
|
|
||||||
dconn, err := dialer.DialContext(ctx, "tcp", dest)
|
|
||||||
if err != nil {
|
|
||||||
newChan.Reject(gossh.ConnectionFailed, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ch, reqs, err := newChan.Accept()
|
|
||||||
if err != nil {
|
|
||||||
dconn.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
go gossh.DiscardRequests(reqs)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer ch.Close()
|
|
||||||
defer dconn.Close()
|
|
||||||
io.Copy(ch, dconn)
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
defer ch.Close()
|
|
||||||
defer dconn.Close()
|
|
||||||
io.Copy(dconn, ch)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
type remoteForwardRequest struct {
|
|
||||||
BindAddr string
|
|
||||||
BindPort uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
type remoteForwardSuccess struct {
|
|
||||||
BindPort uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
type remoteForwardCancelRequest struct {
|
|
||||||
BindAddr string
|
|
||||||
BindPort uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
type remoteForwardChannelData struct {
|
|
||||||
DestAddr string
|
|
||||||
DestPort uint32
|
|
||||||
OriginAddr string
|
|
||||||
OriginPort uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// ForwardedTCPHandler can be enabled by creating a ForwardedTCPHandler and
|
|
||||||
// adding the HandleSSHRequest callback to the server's RequestHandlers under
|
|
||||||
// tcpip-forward and cancel-tcpip-forward.
|
|
||||||
type ForwardedTCPHandler struct {
|
|
||||||
forwards map[string]net.Listener
|
|
||||||
sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ForwardedTCPHandler) HandleSSHRequest(ctx Context, srv *Server, req *gossh.Request) (bool, []byte) {
|
|
||||||
h.Lock()
|
|
||||||
if h.forwards == nil {
|
|
||||||
h.forwards = make(map[string]net.Listener)
|
|
||||||
}
|
|
||||||
h.Unlock()
|
|
||||||
conn := ctx.Value(ContextKeyConn).(*gossh.ServerConn)
|
|
||||||
switch req.Type {
|
|
||||||
case "tcpip-forward":
|
|
||||||
var reqPayload remoteForwardRequest
|
|
||||||
if err := gossh.Unmarshal(req.Payload, &reqPayload); err != nil {
|
|
||||||
// TODO: log parse failure
|
|
||||||
return false, []byte{}
|
|
||||||
}
|
|
||||||
if srv.ReversePortForwardingCallback == nil || !srv.ReversePortForwardingCallback(ctx, reqPayload.BindAddr, reqPayload.BindPort) {
|
|
||||||
return false, []byte("port forwarding is disabled")
|
|
||||||
}
|
|
||||||
addr := net.JoinHostPort(reqPayload.BindAddr, strconv.Itoa(int(reqPayload.BindPort)))
|
|
||||||
ln, err := net.Listen("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
// TODO: log listen failure
|
|
||||||
return false, []byte{}
|
|
||||||
}
|
|
||||||
_, destPortStr, _ := net.SplitHostPort(ln.Addr().String())
|
|
||||||
destPort, _ := strconv.Atoi(destPortStr)
|
|
||||||
h.Lock()
|
|
||||||
h.forwards[addr] = ln
|
|
||||||
h.Unlock()
|
|
||||||
go func() {
|
|
||||||
<-ctx.Done()
|
|
||||||
h.Lock()
|
|
||||||
ln, ok := h.forwards[addr]
|
|
||||||
h.Unlock()
|
|
||||||
if ok {
|
|
||||||
ln.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
c, err := ln.Accept()
|
|
||||||
if err != nil {
|
|
||||||
// TODO: log accept failure
|
|
||||||
break
|
|
||||||
}
|
|
||||||
originAddr, orignPortStr, _ := net.SplitHostPort(c.RemoteAddr().String())
|
|
||||||
originPort, _ := strconv.Atoi(orignPortStr)
|
|
||||||
payload := gossh.Marshal(&remoteForwardChannelData{
|
|
||||||
DestAddr: reqPayload.BindAddr,
|
|
||||||
DestPort: uint32(destPort),
|
|
||||||
OriginAddr: originAddr,
|
|
||||||
OriginPort: uint32(originPort),
|
|
||||||
})
|
|
||||||
go func() {
|
|
||||||
ch, reqs, err := conn.OpenChannel(forwardedTCPChannelType, payload)
|
|
||||||
if err != nil {
|
|
||||||
// TODO: log failure to open channel
|
|
||||||
log.Println(err)
|
|
||||||
c.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
go gossh.DiscardRequests(reqs)
|
|
||||||
go func() {
|
|
||||||
defer ch.Close()
|
|
||||||
defer c.Close()
|
|
||||||
io.Copy(ch, c)
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
defer ch.Close()
|
|
||||||
defer c.Close()
|
|
||||||
io.Copy(c, ch)
|
|
||||||
}()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
h.Lock()
|
|
||||||
delete(h.forwards, addr)
|
|
||||||
h.Unlock()
|
|
||||||
}()
|
|
||||||
return true, gossh.Marshal(&remoteForwardSuccess{uint32(destPort)})
|
|
||||||
|
|
||||||
case "cancel-tcpip-forward":
|
|
||||||
var reqPayload remoteForwardCancelRequest
|
|
||||||
if err := gossh.Unmarshal(req.Payload, &reqPayload); err != nil {
|
|
||||||
// TODO: log parse failure
|
|
||||||
return false, []byte{}
|
|
||||||
}
|
|
||||||
addr := net.JoinHostPort(reqPayload.BindAddr, strconv.Itoa(int(reqPayload.BindPort)))
|
|
||||||
h.Lock()
|
|
||||||
ln, ok := h.forwards[addr]
|
|
||||||
h.Unlock()
|
|
||||||
if ok {
|
|
||||||
ln.Close()
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
default:
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
package ssh
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"encoding/binary"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
)
|
|
||||||
|
|
||||||
func generateSigner() (ssh.Signer, error) {
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return ssh.NewSignerFromKey(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parsePtyRequest(s []byte) (pty Pty, ok bool) {
|
|
||||||
term, s, ok := parseString(s)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
width32, s, ok := parseUint32(s)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
height32, _, ok := parseUint32(s)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pty = Pty{
|
|
||||||
Term: term,
|
|
||||||
Window: Window{
|
|
||||||
Width: int(width32),
|
|
||||||
Height: int(height32),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseWinchRequest(s []byte) (win Window, ok bool) {
|
|
||||||
width32, s, ok := parseUint32(s)
|
|
||||||
if width32 < 1 {
|
|
||||||
ok = false
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
height32, _, ok := parseUint32(s)
|
|
||||||
if height32 < 1 {
|
|
||||||
ok = false
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
win = Window{
|
|
||||||
Width: int(width32),
|
|
||||||
Height: int(height32),
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseString(in []byte) (out string, rest []byte, ok bool) {
|
|
||||||
if len(in) < 4 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
length := binary.BigEndian.Uint32(in)
|
|
||||||
if uint32(len(in)) < 4+length {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
out = string(in[4 : 4+length])
|
|
||||||
rest = in[4+length:]
|
|
||||||
ok = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseUint32(in []byte) (uint32, []byte, bool) {
|
|
||||||
if len(in) < 4 {
|
|
||||||
return 0, nil, false
|
|
||||||
}
|
|
||||||
return binary.BigEndian.Uint32(in), in[4:], true
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
package ssh
|
|
||||||
|
|
||||||
import gossh "golang.org/x/crypto/ssh"
|
|
||||||
|
|
||||||
// PublicKey is an abstraction of different types of public keys.
|
|
||||||
type PublicKey interface {
|
|
||||||
gossh.PublicKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Permissions type holds fine-grained permissions that are specific to a
|
|
||||||
// user or a specific authentication method for a user. Permissions, except for
|
|
||||||
// "source-address", must be enforced in the server application layer, after
|
|
||||||
// successful authentication.
|
|
||||||
type Permissions struct {
|
|
||||||
*gossh.Permissions
|
|
||||||
}
|
|
||||||
|
|
||||||
// A Signer can create signatures that verify against a public key.
|
|
||||||
type Signer interface {
|
|
||||||
gossh.Signer
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseAuthorizedKey parses a public key from an authorized_keys file used in
|
|
||||||
// OpenSSH according to the sshd(8) manual page.
|
|
||||||
func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []string, rest []byte, err error) {
|
|
||||||
return gossh.ParseAuthorizedKey(in)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParsePublicKey parses an SSH public key formatted for use in
|
|
||||||
// the SSH wire protocol according to RFC 4253, section 6.6.
|
|
||||||
func ParsePublicKey(in []byte) (out PublicKey, err error) {
|
|
||||||
return gossh.ParsePublicKey(in)
|
|
||||||
}
|
|
Loading…
Reference in New Issue