Initial commit
This commit is contained in:
commit
448a389515
27
LICENSE
Normal file
27
LICENSE
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
Copyright (c) 2017 the gokrazy authors. 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 gokrazy 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.
|
63
README.md
Normal file
63
README.md
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# breakglass
|
||||||
|
|
||||||
|
breakglass is a [gokrazy](https://github.com/gokrazy/gokrazy) package
|
||||||
|
which provides emergency/debugging access to a gokrazy installation.
|
||||||
|
|
||||||
|
It breaks the gokrazy model in that it allows you to run payloads
|
||||||
|
implemented in any language (e.g. busybox, implemented in C).
|
||||||
|
|
||||||
|
To repeat, breakglass’s whole idea is **remote code execution** (via
|
||||||
|
SSH/SCP, listening only on private network addresss). Hence, it should
|
||||||
|
usually not be present on your gokrazy installation, but it might be
|
||||||
|
useful for development/debugging. As a safety measure, breakglass will
|
||||||
|
not automatically be started on boot, but needs to explicitly be
|
||||||
|
started via the gokrazy web interface.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Add the `github.com/gokrazy/breakglass` package to your `gokr-packer`
|
||||||
|
command, e.g.:
|
||||||
|
|
||||||
|
```
|
||||||
|
gokr-packer -overwrite=/dev/sdb \
|
||||||
|
github.com/gokrazy/hello \
|
||||||
|
github.com/gokrazy/breakglass
|
||||||
|
```
|
||||||
|
|
||||||
|
On the permanent file system of your gokrazy installation, create a
|
||||||
|
host key and an authorized keys file. Assuming you mounted the
|
||||||
|
permanent file system at `/media/sdb4`:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo ssh-keygen -N '' -t rsa -f /media/sdb4/breakglass.host_key
|
||||||
|
sudo install -m 600 ~/.ssh/authorized_keys /media/sdb4/breakglass.authorized_keys
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Create a tarball containing your statically linked arm64 binaries
|
||||||
|
and any other files you’ll need.
|
||||||
|
2. SCP that tarball to your gokrazy installation, where breakglass
|
||||||
|
will unpack it into a temporary directory.
|
||||||
|
3. Execute a binary via SSH.
|
||||||
|
|
||||||
|
Here’s an example, assuming you unpacked and statically cross-compiled
|
||||||
|
busybox in `/tmp/busybox-1.22.0` and your gokrazy installation runs on
|
||||||
|
host `gokrazy`:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cd /tmp/busybox-1.22.0
|
||||||
|
$ file busybox
|
||||||
|
busybox: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked,
|
||||||
|
for GNU/Linux 3.7.0, BuildID[sha1]=c9e20e9849ed0ca3c2bd058427ac31a27c008efe, stripped
|
||||||
|
$ tar cf breakglass.tar busybox
|
||||||
|
$ scp breakglass.tar gokrazy:
|
||||||
|
$ ssh gokrazy -t ./busybox sh
|
||||||
|
/tmp/breakglass564067692 # df -h
|
||||||
|
Filesystem Size Used Available Use% Mounted on
|
||||||
|
/dev/root 60.5M 60.5M 0 100% /
|
||||||
|
devtmpfs 445.3M 0 445.3M 0% /dev
|
||||||
|
tmpfs 50.0M 1.8M 48.2M 4% /tmp
|
||||||
|
tmpfs 1.0M 8.0K 1016.0K 1% /etc
|
||||||
|
/dev/mmcblk0p4 28.2G 44.1M 26.7G 0% /perm
|
||||||
|
```
|
146
breakglass.go
Normal file
146
breakglass.go
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
// breakglass is a SSH/SCP server which unpacks received tar archives
|
||||||
|
// and allows to run commands in the unpacked archive.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/gokrazy/gokrazy"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
authorizedKeysPath = flag.String("authorized_keys",
|
||||||
|
"/perm/breakglass.authorized_keys",
|
||||||
|
"path to an OpenSSH authorized_keys file")
|
||||||
|
|
||||||
|
hostKeyPath = flag.String("host_key",
|
||||||
|
"/perm/breakglass.host_key",
|
||||||
|
"path to a PEM-encoded RSA, DSA or ECDSA private key (create using e.g. ssh-keygen -f /perm/breakglass.host_key -N '' -t rsa)")
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadAuthorizedKeys(path string) (map[string]bool, error) {
|
||||||
|
b, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[string]bool)
|
||||||
|
|
||||||
|
for len(b) > 0 {
|
||||||
|
pubKey, _, _, rest, err := ssh.ParseAuthorizedKey(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result[string(pubKey.Marshal())] = true
|
||||||
|
b = rest
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadHostKey(path string) (ssh.Signer, error) {
|
||||||
|
b, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ssh.ParsePrivateKey(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
|
|
||||||
|
gokrazy.DontStartOnBoot()
|
||||||
|
|
||||||
|
authorizedKeys, err := loadAuthorizedKeys(*authorizedKeysPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &ssh.ServerConfig{
|
||||||
|
PublicKeyCallback: func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
|
||||||
|
if authorizedKeys[string(pubKey.Marshal())] {
|
||||||
|
log.Printf("user %q successfully authorized from remote addr %s", conn.User(), conn.RemoteAddr())
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("public key not found in %s", *authorizedKeysPath)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
signer, err := loadHostKey(*hostKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
config.AddHostKey(signer)
|
||||||
|
|
||||||
|
unpackDir, err := ioutil.TempDir("", "breakglass")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(unpackDir)
|
||||||
|
|
||||||
|
// This tmpfs mount ensures that our temp directory is mounted
|
||||||
|
// without NOEXEC and that we have plenty of space for payload.
|
||||||
|
// It will be cleaned up on process exit because each gokrazy
|
||||||
|
// process uses a non-shared mount namespace.
|
||||||
|
if err := syscall.Mount("tmpfs", unpackDir, "tmpfs", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_RELATIME, "size=500M"); err != nil {
|
||||||
|
log.Fatal("tmpfs on %s: %v", unpackDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Chdir(unpackDir); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
accept := func(listener net.Listener) {
|
||||||
|
for {
|
||||||
|
conn, err := listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("accept: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(conn net.Conn) {
|
||||||
|
_, chans, reqs, err := ssh.NewServerConn(conn, config)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("handshake: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// discard all out of band requests
|
||||||
|
go ssh.DiscardRequests(reqs)
|
||||||
|
|
||||||
|
for newChannel := range chans {
|
||||||
|
handleChannel(newChannel)
|
||||||
|
}
|
||||||
|
}(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs, err := gokrazy.PrivateInterfaceAddrs()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
hostport := net.JoinHostPort(addr, "22")
|
||||||
|
listener, err := net.Listen("tcp", hostport)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("listening on %s\n", hostport)
|
||||||
|
go accept(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("host key fingerprint: %s\n", ssh.FingerprintSHA256(signer.PublicKey()))
|
||||||
|
|
||||||
|
select {}
|
||||||
|
}
|
124
scp.go
Normal file
124
scp.go
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type countingWriter int64
|
||||||
|
|
||||||
|
func (cw *countingWriter) Write(p []byte) (n int, err error) {
|
||||||
|
*cw += countingWriter(len(p))
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func scpSink(channel ssh.Channel, req *ssh.Request, cmdline []string) error {
|
||||||
|
scpFlags := flag.NewFlagSet("scp", flag.ContinueOnError)
|
||||||
|
sink := scpFlags.Bool("t", false, "sink (to)")
|
||||||
|
if err := scpFlags.Parse(cmdline[1:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !*sink {
|
||||||
|
return fmt.Errorf("expected -t")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell the remote end we’re ready to receive data.
|
||||||
|
if _, err := channel.Write([]byte{0x00}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
for {
|
||||||
|
n, err := channel.Read(buf)
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
msg := buf[:n]
|
||||||
|
|
||||||
|
// Acknowledge receipt of the control message
|
||||||
|
if _, err := channel.Write([]byte{0x00}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg[0] == 'C' {
|
||||||
|
msgstr := strings.TrimSpace(string(msg))
|
||||||
|
parts := strings.Split(msgstr, " ")
|
||||||
|
if got, want := len(parts), 3; got != want {
|
||||||
|
return fmt.Errorf("invalid number of space-separated tokens in control message %q: got %d, want %d", msgstr, got, want)
|
||||||
|
}
|
||||||
|
size, err := strconv.ParseInt(parts[1], 0, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve file contents
|
||||||
|
var cw countingWriter
|
||||||
|
tr := tar.NewReader(io.TeeReader(channel, &cw))
|
||||||
|
for {
|
||||||
|
h, err := tr.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("extracting %q", h.Name)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(h.Name), 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mode := h.FileInfo().Mode() & os.ModePerm
|
||||||
|
out, err := os.OpenFile(h.Name, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(out, tr); err != nil {
|
||||||
|
out.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := out.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rest := size - int64(cw); rest > 0 {
|
||||||
|
buf := make([]byte, rest)
|
||||||
|
if _, err := channel.Read(buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read status byte after transfer
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
if _, err := channel.Read(buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acknowledge file transfer
|
||||||
|
if _, err := channel.Write([]byte{0x00}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exitStatus := make([]byte, 4)
|
||||||
|
exitStatus[3] = 0
|
||||||
|
if _, err := channel.SendRequest("exit-status", false, exitStatus); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
channel.Close()
|
||||||
|
req.Reply(true, nil)
|
||||||
|
return nil
|
||||||
|
}
|
227
ssh.go
Normal file
227
ssh.go
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/google/shlex"
|
||||||
|
"github.com/kr/pty"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleChannel(newChannel ssh.NewChannel) {
|
||||||
|
if t := newChannel.ChannelType(); t != "session" {
|
||||||
|
newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %q", t))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
channel, requests, err := newChannel.Accept()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not accept channel (%s)", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sessions have out-of-band requests such as "shell", "pty-req" and "env"
|
||||||
|
go func(channel ssh.Channel, requests <-chan *ssh.Request) {
|
||||||
|
s := session{channel: channel}
|
||||||
|
for req := range requests {
|
||||||
|
if err := s.request(req); err != nil {
|
||||||
|
errmsg := []byte(err.Error())
|
||||||
|
// Append a trailing newline; the error message is
|
||||||
|
// displayed as-is by ssh(1).
|
||||||
|
if errmsg[len(errmsg)-1] != '\n' {
|
||||||
|
errmsg = append(errmsg, '\n')
|
||||||
|
}
|
||||||
|
req.Reply(false, errmsg)
|
||||||
|
channel.Write(errmsg)
|
||||||
|
channel.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(channel, requests)
|
||||||
|
}
|
||||||
|
|
||||||
|
type session struct {
|
||||||
|
env []string
|
||||||
|
ptyf *os.File
|
||||||
|
ttyf *os.File
|
||||||
|
channel ssh.Channel
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringFromPayload(payload []byte, offset int) (string, int, error) {
|
||||||
|
if got, want := len(payload), offset+4; got < want {
|
||||||
|
return "", 0, fmt.Errorf("request payload too short: got %d, want >= %d", got, want)
|
||||||
|
}
|
||||||
|
namelen := binary.BigEndian.Uint32(payload[offset : offset+4])
|
||||||
|
if got, want := len(payload), offset+4+int(namelen); got < want {
|
||||||
|
return "", 0, fmt.Errorf("request payload too short: got %d, want >= %d", got, want)
|
||||||
|
}
|
||||||
|
name := payload[offset+4 : offset+4+int(namelen)]
|
||||||
|
return string(name), offset + 4 + int(namelen), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) request(req *ssh.Request) error {
|
||||||
|
switch req.Type {
|
||||||
|
case "pty-req":
|
||||||
|
var err error
|
||||||
|
s.ptyf, s.ttyf, err = pty.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, next, err := stringFromPayload(req.Payload, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if got, want := len(req.Payload), next+4+4; got < want {
|
||||||
|
return fmt.Errorf("request payload too short: got %d, want >= %d", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
w, h := parseDims(req.Payload[next:])
|
||||||
|
SetWinsize(s.ptyf.Fd(), w, h)
|
||||||
|
// Responding true (OK) here will let the client
|
||||||
|
// know we have a pty ready for input
|
||||||
|
req.Reply(true, nil)
|
||||||
|
|
||||||
|
case "window-change":
|
||||||
|
w, h := parseDims(req.Payload)
|
||||||
|
SetWinsize(s.ptyf.Fd(), w, h)
|
||||||
|
|
||||||
|
case "env":
|
||||||
|
name, next, err := stringFromPayload(req.Payload, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
value, _, err := stringFromPayload(req.Payload, next)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.env = append(s.env, fmt.Sprintf("%s=%s", name, value))
|
||||||
|
|
||||||
|
case "shell":
|
||||||
|
// as per https://tools.ietf.org/html/rfc4254#section-6.5,
|
||||||
|
// shell requests don’t carry a payload, and we don’t have a
|
||||||
|
// default shell, so decline the request
|
||||||
|
return fmt.Errorf("shell requests unsupported, use exec")
|
||||||
|
|
||||||
|
case "exec":
|
||||||
|
if got, want := len(req.Payload), 4; got < want {
|
||||||
|
return fmt.Errorf("exec request payload too short: got %d, want >= %d", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdline, err := shlex.Split(string(req.Payload[4:]))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmdline[0] == "scp" {
|
||||||
|
return scpSink(s.channel, req, cmdline)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(cmdline[0], cmdline[1:]...)
|
||||||
|
cmd.Env = s.env
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||||
|
|
||||||
|
if s.ttyf == nil {
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd.SysProcAttr.Setsid = true
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go io.Copy(s.channel, stdout)
|
||||||
|
go io.Copy(s.channel, stderr)
|
||||||
|
go func() {
|
||||||
|
io.Copy(stdin, s.channel)
|
||||||
|
stdin.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.channel.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
s.ttyf.Close()
|
||||||
|
s.ttyf = nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
cmd.Stdout = s.ttyf
|
||||||
|
cmd.Stdin = s.ttyf
|
||||||
|
cmd.Stderr = s.ttyf
|
||||||
|
cmd.SysProcAttr.Setctty = true
|
||||||
|
cmd.SysProcAttr.Setsid = true
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
s.ptyf.Close()
|
||||||
|
s.ptyf = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
close := func() {
|
||||||
|
s.channel.Close()
|
||||||
|
cmd.Process.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// pipe session to cmd and vice-versa
|
||||||
|
var once sync.Once
|
||||||
|
go func() {
|
||||||
|
io.Copy(s.channel, s.ptyf)
|
||||||
|
once.Do(close)
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
io.Copy(s.ptyf, s.channel)
|
||||||
|
once.Do(close)
|
||||||
|
}()
|
||||||
|
|
||||||
|
req.Reply(true, nil)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown request type: %q", req.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseDims extracts terminal dimensions (width x height) from the provided buffer.
|
||||||
|
func parseDims(b []byte) (uint32, uint32) {
|
||||||
|
w := binary.BigEndian.Uint32(b)
|
||||||
|
h := binary.BigEndian.Uint32(b[4:])
|
||||||
|
return w, h
|
||||||
|
}
|
||||||
|
|
||||||
|
// Winsize stores the Height and Width of a terminal.
|
||||||
|
type Winsize struct {
|
||||||
|
Height uint16
|
||||||
|
Width uint16
|
||||||
|
x uint16 // unused
|
||||||
|
y uint16 // unused
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetWinsize sets the size of the given pty.
|
||||||
|
func SetWinsize(fd uintptr, w, h uint32) {
|
||||||
|
ws := &Winsize{Width: uint16(w), Height: uint16(h)}
|
||||||
|
syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(ws)))
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user